Compare commits

...
Author SHA1 Message Date
Glavo d3d46f6a29 Fix #5039: 修复陶瓦联机页面部分文本在深色模式下颜色异常的问题 (#5040) 2025-12-21 15:13:58 +08:00
Glavo 6f1db67e9a 优化反馈页面 (#5008)
1. 将主页左下角“官方群组”按钮调整为跳转到“反馈”页面;
2. 将反馈页面分为“官方群组”和“提交反馈”两部分
2025-12-20 20:35:41 +08:00
mineDiamond 0d7ece819d feat: 添加快速进入世界功能 (#4872)
close #2563
2025-12-20 20:33:52 +08:00
Damon Lu 7f852080cd 在 macOS 上添加启动脚本的后缀 .command (#4927) 2025-12-20 20:32:34 +08:00
neveler e0360c2b38 使用 subtitle 显示预览更新提示 (#5019) 2025-12-20 16:04:20 +08:00
Glavo 3bf0eb2a16 Fix #5014: 修复 GameCrashWindow 中 feedbackTextFlow 样式错误的问题 (#5020) 2025-12-20 16:00:42 +08:00
Glavo 84b428b57c 在 JFXCustomColorPickerDialog 中使用 HEX 作为默认 Tab (#5011) 2025-12-20 15:33:03 +08:00
neveler 41895e97e1 修复 subtitle 颜色丢失的问题 (#5018)
fix #5017
2025-12-20 15:32:34 +08:00
zimzaza4 f839dc192c 修改AccountAdvancedListItem 控件, 在 RootPage 中添加快速选择账户 (#4932) 2025-12-19 22:18:10 +08:00
Glavo 24250a6239 更新 CNB Issue 模板 (#5010) 2025-12-19 22:03:43 +08:00
Glavo a3ab2b52b6 添加 CNB Issue 模板 (#5009) 2025-12-19 21:39:00 +08:00
Glavo c4d4c6fcf3 优化镜像仓库同步功能 (#5006) 2025-12-19 16:47:12 +08:00
Glavo f77f005f88 mirror.yml 中直接传递用户名 (#5005) 2025-12-19 16:10:02 +08:00
Glavo 9ec494664c 简化镜像仓库同步 workflow (#5004) 2025-12-19 16:02:02 +08:00
Glavo dc7c883962 在低于 JavaFX 23 的情况下隐藏主题模式跟随系统选项 (#4985) 2025-12-18 21:25:07 +08:00
Glavo 567bd15997 在陶瓦联机界面启动游戏时禁用离线皮肤功能 (#4916) 2025-12-18 21:18:03 +08:00
Glavo 25c9ef0f27 修复无法解析类似 "26.1 Snapshot 1" 的版本号的问题 (#5002) 2025-12-18 21:09:07 +08:00
Glavo d45d994307 将 Release 同步至 CNB (#5000) 2025-12-18 21:08:30 +08:00
Glavo 01b7a6b95a Revert "修复 TransitionPane 动画被打断时节点状态异常的问题" (#4986)
Reverts HMCL-dev/HMCL#4957
2025-12-18 20:47:48 +08:00
神奇的赵二 (OrzMiku) a0593c03c6 支持下载 Java25 (#4990) 2025-12-18 20:46:52 +08:00
Glavo 91f05315aa Add CNB badge (#4999) 2025-12-18 20:11:06 +08:00
Burning_TNT 84b8fff37b Bump authlib-injector to 1.2.7: fix compatibility with 26.1-snapshot-1 (#4992)
Fix #4989
2025-12-17 17:05:07 -05:00
Glavo 3a9cc2b9bb 修复 CNB 同步功能 (#4995) 2025-12-18 05:57:14 +08:00
Glavo 056f30f6f5 将代码同步至 CNB (#4994) 2025-12-18 05:47:29 +08:00
辞庐 2d6e01fcf9 添加输出游戏调试日志选项 (#4144) 2025-12-14 20:49:23 +08:00
Calboot b6d95c3ebb [Enhancement] 给 TableView 添加样式 (#4854) 2025-12-12 22:00:55 +08:00
Glavo 29e40ec1a5 在 DownloadListPage 中添加图标缓存 (#4866) 2025-12-12 21:12:51 +08:00
Glavo 497789603a 在日志中记录显示器信息 (#4581) 2025-12-11 20:47:10 +08:00
Glavo 8d3cb66d17 将 TableView 中的 CheckBox 替换为 JFXCheckBox (#4964) 2025-12-11 20:41:26 +08:00
mineDiamond 0613b342f0 feat: 添加1.21.11_unobfuscated支持 (#4963) 2025-12-11 20:29:55 +08:00
Glavo 18000fae30 修复 TransitionPane 动画被打断时节点状态异常的问题 (#4957) 2025-12-09 15:32:17 +08:00
Glavo d2c985491a 合并 SettingsPage 与 SettingsView (#4955) 2025-12-08 16:45:29 +08:00
Glavo f7f0d55504 更新 RipplerContainer 的涟漪颜色 (#4953) 2025-12-08 15:17:10 +08:00
Glavo 210e127ac7 Fix #4950: 修复部分按钮波纹颜色错误的问题 (#4952) 2025-12-07 21:46:42 +08:00
Glavo 36748a3a07 Fix #4941: 更新 JFXComboBox 下拉菜单滚动条样式 (#4951) 2025-12-07 21:43:47 +08:00
Burning_TNT cf667b2b8d Fix #4759: 部分 LF/NF 模组无法正确识别 (#4945) 2025-12-07 21:13:49 +08:00
Burning_TNT a3f7a230e7 Fix #4597: 绕过 OptiFine HD U G6 pre1 与其他模组 Fingerprint 冲突的问题 (#4946) 2025-12-07 21:12:08 +08:00
辞庐 9dd8084be2 修复本地安装新版本 Forge 时未移除版本号中 forge- 前缀的问题 (#4935) 2025-12-07 21:04:18 +08:00
Glavo 9cf5a5e5db 修复从 BMCLAPI 下载部分旧文件时校验失败的问题 (#4949) 2025-12-07 20:45:02 +08:00
Glavo 8efa54dd11 更新超链接被聚焦时的边框样式 (#4948) 2025-12-07 20:44:32 +08:00
Wulian233 7902020300 Fix #4939: 修复深色模式超链接的已访问样式配色问题 (#4943) 2025-12-07 16:35:46 +08:00
Glavo e72c0bbb50 修复部分控件 Tooltip 文字颜色异常的问题 (#4947) 2025-12-07 16:15:22 +08:00
ef3df92592 feat: 资源包管理 (#4475)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-12-06 22:17:04 +08:00
Glavo 04b300f453 简化 MessageDialogPane (#4918) 2025-12-06 21:18:58 +08:00
mineDiamondandGlavo e455ce4c84 feat: 支持1.21.11-pre4/5/rc1/2_unobfuscated (#4911)
Co-authored-by: Glavo <zjx001202@gmail.com>
2025-12-06 21:03:27 +08:00
Glavo 456d13959e 更新 GameVersionNumber 解析规则 (#4917)
1. 支持 [Minecraft
新版本号方案](https://www.minecraft.net/en-us/article/minecraft-new-version-numbering-system)
2. 支持对版本号进行归一化处理。
2025-12-06 20:55:11 +08:00
Glavo 7ac22ad567 Windows 11 上日志窗口标题栏颜色应当跟随主题模式设置 (#4910) 2025-12-04 01:24:36 +08:00
neveler ed4fec6c42 在 HintPane 提示框中为图标和文本添加间距 (#4915) 2025-12-03 16:45:44 +08:00
Glavo c321453399 Fix #4900: 修复 ManagedProcess#getLines 完成后不会解除锁定的问题 (#4913) 2025-12-03 16:19:17 +08:00
Calboot aae02bef50 [Bugfix] 修复模组更新列表界面按 esc 无法正确退出的问题 (#4895)
Fixes #4894
2025-12-03 16:17:46 +08:00
Glavo 89c9a93aab 优化深色模式下日志窗口筛选按钮的颜色 (#4912) 2025-12-03 16:04:10 +08:00
neveler 74ec4aaae8 优化检查更新工作流 (#4879) 2025-12-02 21:15:43 +08:00
Glavo 669a9dfc57 优化更新弹窗的颜色 (#4903) 2025-12-02 21:14:10 +08:00
Glavo a1e75e48da 修复部分控件在深色模式下颜色异常的问题 (#4899) 2025-12-02 20:29:46 +08:00
109 changed files with 2972 additions and 1112 deletions

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: QQ 群
url: https://docs.hmcl.net/groups.html
about: Hello Minecraft! Launcher 的官方 QQ 交流群。
- name: Discord 服务器
url: https://discord.gg/jVvC7HfM6U
about: Hello Minecraft! Launcher 的官方 Discord 服务器。
-79
View File
@@ -1,79 +0,0 @@
name: Check Update
on:
workflow_dispatch:
# schedule:
# - cron: '30 * * * *'
permissions:
contents: write
jobs:
check-update:
if: ${{ github.repository_owner == 'HMCL-dev' }}
strategy:
fail-fast: false
max-parallel: 1
matrix:
include:
- channel: dev
task: checkUpdateDev
- channel: stable
task: checkUpdateStable
runs-on: ubuntu-latest
name: check-update-${{ matrix.channel }}
steps:
- uses: actions/checkout@v5
- name: Set up JDK
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '25'
- name: Fetch tags
run: git fetch --all --tags
- name: Fetch last version
run: ./gradlew ${{ matrix.task }} --no-daemon --info --stacktrace
- name: Check for existing tags
run: if [ -z "$(git tag -l "$HMCL_TAG_NAME")" ]; then echo "continue=true" >> $GITHUB_ENV; fi
- name: Download artifacts
if: ${{ env.continue == 'true' }}
run: |
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.exe"
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.exe.sha256"
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.jar"
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.jar.sha256"
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh"
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh.sha256"
env:
GH_DOWNLOAD_BASE_URL: https://github.com/HMCL-dev/HMCL/releases/download
- name: Generate release note
if: ${{ env.continue == 'true' }}
run: |
if [ "${{ matrix.channel }}" = "stable" ]; then
echo "**This version is a stable version.**" >> RELEASE_NOTE
echo "" >> RELEASE_NOTE
fi
echo "The full changelogs can be found on the website: https://docs.hmcl.net/changelog/${{ matrix.channel }}.html" >> RELEASE_NOTE
echo "" >> RELEASE_NOTE
echo "*Notice: changelogs are written in Chinese.*" >> RELEASE_NOTE
echo "" >> RELEASE_NOTE
echo "| File Name | SHA-256 Checksum |" >> RELEASE_NOTE
echo "| --- | --- |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.exe]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | \`$(cat HMCL-$HMCL_VERSION.exe.sha256)\` |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.jar]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | \`$(cat HMCL-$HMCL_VERSION.jar.sha256)\` |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.sh]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | \`$(cat HMCL-$HMCL_VERSION.sh.sha256)\` |" >> RELEASE_NOTE
env:
GH_DOWNLOAD_BASE_URL: https://github.com/HMCL-dev/HMCL/releases/download
- name: Create release
if: ${{ env.continue == 'true' }}
uses: softprops/action-gh-release@v2
with:
body_path: RELEASE_NOTE
files: |
HMCL-${{ env.HMCL_VERSION }}.exe
HMCL-${{ env.HMCL_VERSION }}.jar
HMCL-${{ env.HMCL_VERSION }}.sh
target_commitish: ${{ env.HMCL_COMMIT_SHA }}
name: ${{ env.HMCL_TAG_NAME }}
tag_name: ${{ env.HMCL_TAG_NAME }}
prerelease: true
-21
View File
@@ -1,21 +0,0 @@
name: Sync to Gitee
on:
push
jobs:
run:
if: ${{ github.repository_owner == 'HMCL-dev' }}
runs-on: ubuntu-latest
steps:
- name: Mirror GitHub to Gitee
uses: Yikun/hub-mirror-action@v1.4
with:
src: github/HMCL-dev
dst: gitee/huanghongxun
static_list: 'HMCL'
force_update: true
debug: true
dst_key: ${{ secrets.GITEE_SYNC_BOT_PRIVATE_KEY }}
dst_token: ${{ secrets.GITEE_SYNC_BOT_TOKEN }}
cache_path: /github/workspace/hub-mirror-cache
+32
View File
@@ -0,0 +1,32 @@
name: Mirror Repository
on:
workflow_dispatch:
push:
concurrency:
group: mirror-repository
cancel-in-progress: true
jobs:
mirror:
strategy:
matrix:
target:
- name: Gitee
repo: gitee.com/huanghongxun/HMCL
user: 'hmcl-sync'
token: 'GITEE_SYNC_TOKEN'
- name: CNB
repo: cnb.cool/HMCL-dev/HMCL
user: 'cnb'
token: 'CNB_SYNC_TOKEN'
name: Mirror to ${{ matrix.target.name }}
if: ${{ github.repository == 'HMCL-dev/HMCL' }}
runs-on: ubuntu-latest
steps:
- name: Mirror GitHub to ${{ matrix.target.name }}
run: |
git clone --mirror "https://github.com/$GITHUB_REPOSITORY.git" -- repo
cd repo
git push -f --prune "https://${{ matrix.target.user }}:${{ secrets[matrix.target.token] }}@${{ matrix.target.repo }}.git" "refs/heads/*:refs/heads/*" "refs/tags/*:refs/tags/*"
+125
View File
@@ -0,0 +1,125 @@
name: Create Release
on:
workflow_dispatch:
# schedule:
# - cron: '30 * * * *'
permissions:
contents: write
jobs:
check-update:
if: ${{ github.repository_owner == 'HMCL-dev' }}
strategy:
fail-fast: false
max-parallel: 1
matrix:
include:
- channel: dev
task: checkUpdateDev
- channel: stable
task: checkUpdateStable
runs-on: ubuntu-latest
name: check-update-${{ matrix.channel }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
fetch-tags: true
- name: Set up JDK
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '25'
- name: Fetch last version
run: ./gradlew ${{ matrix.task }} --no-daemon --info --stacktrace
- name: Check for existing tags
run: if [ -z "$(git tag -l "$HMCL_TAG_NAME")" ]; then echo "continue=true" >> $GITHUB_ENV; fi
- name: Download artifacts
if: ${{ env.continue == 'true' }}
run: |
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.exe"
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.exe.sha256"
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.jar"
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.jar.sha256"
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh"
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh.sha256"
- name: Verify artifacts
if: ${{ env.continue == 'true' }}
run: |
export JAR_SHA256=$(cat HMCL-$HMCL_VERSION.jar.sha256 | tr -d '\n')
export EXE_SHA256=$(cat HMCL-$HMCL_VERSION.exe.sha256 | tr -d '\n')
export SH_SHA256=$(cat HMCL-$HMCL_VERSION.sh.sha256 | tr -d '\n')
echo "$JAR_SHA256 HMCL-$HMCL_VERSION.jar" | sha256sum -c
echo "$EXE_SHA256 HMCL-$HMCL_VERSION.exe" | sha256sum -c
echo "$SH_SHA256 HMCL-$HMCL_VERSION.sh" | sha256sum -c
- name: Generate release note
if: ${{ env.continue == 'true' }}
run: |
# GitHub Release Note
echo "&nbsp;**[Changelog](https://docs.hmcl.net/changelog/${{ matrix.channel }}.html#HMCL-$HMCL_VERSION)** (Chinese)" >> RELEASE_NOTE
echo "" >> RELEASE_NOTE
echo "| File | SHA-256 Checksum |" >> RELEASE_NOTE
echo "| --- | --- |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.exe]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | \`$(cat HMCL-$HMCL_VERSION.exe.sha256)\` |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.jar]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | \`$(cat HMCL-$HMCL_VERSION.jar.sha256)\` |" >> RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.sh]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | \`$(cat HMCL-$HMCL_VERSION.sh.sha256)\` |" >> RELEASE_NOTE
# CNB Release Note
echo "[更新日志](https://docs.hmcl.net/changelog/${{ matrix.channel }}.html#HMCL-$HMCL_VERSION)" >> CNB_RELEASE_NOTE
echo "" >> CNB_RELEASE_NOTE
echo "| 文件 | SHA-256 校验码 |" >> CNB_RELEASE_NOTE
echo "| :--- | --- |" >> CNB_RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.exe]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | \`$(cat HMCL-$HMCL_VERSION.exe.sha256)\` |" >> CNB_RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.jar]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | \`$(cat HMCL-$HMCL_VERSION.jar.sha256)\` |" >> CNB_RELEASE_NOTE
echo "| [HMCL-$HMCL_VERSION.sh]($CNB_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | \`$(cat HMCL-$HMCL_VERSION.sh.sha256)\` |" >> CNB_RELEASE_NOTE
env:
GH_DOWNLOAD_BASE_URL: https://github.com/${{ github.repository }}/releases/download
CNB_DOWNLOAD_BASE_URL: https://cnb.cool/HMCL-dev/HMCL/-/releases/download
- name: Create GitHub release
if: ${{ env.continue == 'true' }}
run: |
gh release create "${{ env.HMCL_TAG_NAME }}" \
"HMCL-${{ env.HMCL_VERSION }}.exe" \
"HMCL-${{ env.HMCL_VERSION }}.jar" \
"HMCL-${{ env.HMCL_VERSION }}.sh" \
--target "${{ env.HMCL_COMMIT_SHA }}" \
--title "${{ env.HMCL_TAG_NAME }}" \
--notes-file RELEASE_NOTE \
--prerelease
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install git-cnb
if: ${{ env.continue == 'true' }}
run: go install "cnb.cool/looc/git-cnb@$GIT_CNB_VERSION"
env:
GIT_CNB_VERSION: '1.1.2'
- name: Create CNB release
if: ${{ env.continue == 'true' }}
run: |
echo "Uploading tags to CNB"
git fetch --tags
git push "https://cnb:${{ secrets.CNB_SYNC_TOKEN }}@cnb.cool/$CNB_REPO.git" "$HMCL_TAG_NAME"
echo "Creating CNB release"
~/go/bin/git-cnb release create \
--repo "$CNB_REPO" \
--tag "$HMCL_TAG_NAME" \
--name "HMCL $HMCL_VERSION" \
--body "$(cat CNB_RELEASE_NOTE)" \
--prerelease true
echo "Uploading HMCL-$HMCL_VERSION.jar"
~/go/bin/git-cnb release asset-upload --repo="$CNB_REPO" --tag-name "$HMCL_TAG_NAME" --file-name "HMCL-$HMCL_VERSION.jar"
echo "Uploading HMCL-$HMCL_VERSION.exe"
~/go/bin/git-cnb release asset-upload --repo="$CNB_REPO" --tag-name "$HMCL_TAG_NAME" --file-name "HMCL-$HMCL_VERSION.exe"
echo "Uploading HMCL-$HMCL_VERSION.sh"
~/go/bin/git-cnb release asset-upload --repo="$CNB_REPO" --tag-name "$HMCL_TAG_NAME" --file-name "HMCL-$HMCL_VERSION.sh"
env:
CNB_TOKEN: ${{ secrets.CNB_SYNC_TOKEN }}
CNB_REPO: HMCL-dev/HMCL
+2
View File
@@ -128,11 +128,13 @@ val addOpens = listOf(
"javafx.base/javafx.beans.property",
"javafx.graphics/javafx.css",
"javafx.graphics/javafx.stage",
"javafx.graphics/com.sun.glass.ui",
"javafx.graphics/com.sun.javafx.stage",
"javafx.graphics/com.sun.javafx.util",
"javafx.graphics/com.sun.prism",
"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.skin",
"jdk.attach/sun.tools.attach",
)
@@ -28,6 +28,8 @@ import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.*;
import javafx.css.converter.BooleanConverter;
import javafx.geometry.Insets;
@@ -60,6 +62,9 @@ public final class JFXColorPickerSkin extends JFXGenericPickerSkin<Color> {
"colorLabelVisible",
true);
private final ObjectProperty<Color> textFill = new SimpleObjectProperty<>(Color.BLACK);
private final ObjectProperty<Background> colorBoxBackground = new SimpleObjectProperty<>(null);
public JFXColorPickerSkin(final ColorPicker colorPicker) {
super(colorPicker);
@@ -67,14 +72,16 @@ public final class JFXColorPickerSkin extends JFXGenericPickerSkin<Color> {
displayNode = new Label("");
displayNode.getStyleClass().add("color-label");
displayNode.setMouseTransparent(true);
displayNode.textFillProperty().bind(textFill);
// label graphic
colorBox = new JFXClippedPane(displayNode);
colorBox.getStyleClass().add("color-box");
colorBox.setManaged(false);
colorBox.backgroundProperty().bind(colorBoxBackground);
initColor();
final JFXRippler rippler = new JFXRippler(colorBox, JFXRippler.RipplerMask.FIT);
rippler.ripplerFillProperty().bind(displayNode.textFillProperty());
rippler.ripplerFillProperty().bind(textFill);
getChildren().setAll(rippler);
JFXDepthManager.setDepth(getSkinnable(), 1);
getSkinnable().setPickOnBounds(false);
@@ -168,13 +175,13 @@ public final class JFXColorPickerSkin extends JFXGenericPickerSkin<Color> {
200,
Interpolator.EASE_BOTH)));
animateColor.setOnFinished((finish) -> {
JFXNodeUtils.updateBackground(colorBox.getBackground(), colorBox, colorCircle.getFill());
colorBoxBackground.set(new Background(new BackgroundFill(colorCircle.getFill(), new CornerRadii(3), Insets.EMPTY)));
colorBox.getChildren().remove(colorCircle);
});
animateColor.play();
}
// update label color
displayNode.setTextFill(circleColor.grayscale().getRed() < 0.5 ? Color.valueOf(
textFill.set(circleColor.grayscale().getRed() < 0.5 ? Color.valueOf(
"rgba(255, 255, 255, 0.87)") : Color.valueOf("rgba(0, 0, 0, 0.87)"));
if (colorLabelVisible.get()) {
displayNode.setText(JFXNodeUtils.colorToHex(circleColor));
@@ -188,9 +195,9 @@ public final class JFXColorPickerSkin extends JFXGenericPickerSkin<Color> {
Color color = colorPicker.getValue();
Color circleColor = color == null ? Color.WHITE : color;
// update picker box color
colorBox.setBackground(new Background(new BackgroundFill(circleColor, new CornerRadii(3), Insets.EMPTY)));
colorBoxBackground.set(new Background(new BackgroundFill(circleColor, new CornerRadii(3), Insets.EMPTY)));
// update label color
displayNode.setTextFill(circleColor.grayscale().getRed() < 0.5 ? Color.valueOf(
textFill.set(circleColor.grayscale().getRed() < 0.5 ? Color.valueOf(
"rgba(255, 255, 255, 0.87)") : Color.valueOf("rgba(0, 0, 0, 0.87)"));
if (colorLabelVisible.get()) {
displayNode.setText(JFXNodeUtils.colorToHex(circleColor));
@@ -32,9 +32,7 @@ import javafx.beans.property.SimpleObjectProperty;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.*;
@@ -118,9 +116,9 @@ public class JFXCustomColorPickerDialog extends StackPane {
Tab hexTab = new Tab("HEX");
hexTab.setContent(hexField);
tabs.getTabs().add(hexTab);
tabs.getTabs().add(rgbTab);
tabs.getTabs().add(hsbTab);
tabs.getTabs().add(hexTab);
curvedColorPicker.selectedPath.addListener((o, oldVal, newVal) -> {
if (paraTransition != null) {
@@ -156,15 +154,15 @@ public class JFXCustomColorPickerDialog extends StackPane {
Color fontColor = ((Color) newVal.getFills().get(0).getFill()).grayscale()
.getRed() > 0.5 ? Color.valueOf(
"rgba(40, 40, 40, 0.87)") : Color.valueOf("rgba(255, 255, 255, 0.87)");
for (Node tabNode : tabs.lookupAll(".tab")) {
for (Node node : tabNode.lookupAll(".tab-label")) {
((Label) node).setTextFill(fontColor);
}
for (Node node : tabNode.lookupAll(".jfx-rippler")) {
((JFXRippler) node).setRipplerFill(fontColor);
}
}
((Pane) tabs.lookup(".tab-selected-line")).setBackground(new Background(new BackgroundFill(fontColor, CornerRadii.EMPTY, Insets.EMPTY)));
// for (Node tabNode : tabs.lookupAll(".tab")) {
// for (Node node : tabNode.lookupAll(".tab-label")) {
// ((Label) node).setTextFill(fontColor);
// }
// for (Node node : tabNode.lookupAll(".jfx-rippler")) {
// ((JFXRippler) node).setRipplerFill(fontColor);
// }
// }
// ((Pane) tabs.lookup(".tab-selected-line")).setBackground(new Background(new BackgroundFill(fontColor, CornerRadii.EMPTY, Insets.EMPTY)));
pickerDecorator.lookupAll(".jfx-decorator-button").forEach(button -> {
((JFXButton) button).setRipplerFill(fontColor);
((SVGGlyph) ((JFXButton) button).getGraphic()).setFill(fontColor);
@@ -20,11 +20,13 @@ package org.jackhuang.hmcl;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ObservableBooleanValue;
import javafx.geometry.Rectangle2D;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.ButtonType;
import javafx.scene.input.Clipboard;
import javafx.scene.input.DataFormat;
import javafx.stage.Screen;
import javafx.stage.Stage;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.setting.SambaException;
@@ -54,6 +56,7 @@ import java.net.CookieManager;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Objects;
@@ -79,6 +82,18 @@ public final class Launcher extends Application {
LOG.info("Dark Mode: " + Optional.ofNullable(FXUtils.DARK_MODE).map(ObservableBooleanValue::get).orElse(false));
LOG.info("Reduced Motion: " + Objects.requireNonNullElse(FXUtils.REDUCED_MOTION, false));
if (Screen.getScreens().isEmpty()) {
LOG.info("No screen");
} else {
StringBuilder builder = new StringBuilder("Screens:");
int count = 0;
for (Screen screen : Screen.getScreens()) {
builder.append("\n - Screen ").append(++count).append(": ");
appendScreen(builder, screen);
}
LOG.info(builder.toString());
}
try {
try {
ConfigHolder.init();
@@ -131,6 +146,38 @@ public final class Launcher extends Application {
}
}
private static void appendScreen(StringBuilder builder, Screen screen) {
Rectangle2D bounds = screen.getBounds();
double scale = screen.getOutputScaleX();
builder.append(Math.round(bounds.getWidth() * scale));
builder.append('x');
builder.append(Math.round(bounds.getHeight() * scale));
DecimalFormat decimalFormat = new DecimalFormat("#.##");
if (scale != 1.0) {
builder.append(" @ ");
builder.append(decimalFormat.format(scale));
builder.append('x');
}
double dpi = screen.getDpi();
builder.append(' ');
builder.append(decimalFormat.format(dpi));
builder.append("dpi");
builder.append(" in ")
.append(Math.round(Math.sqrt(bounds.getWidth() * bounds.getWidth() + bounds.getHeight() * bounds.getHeight()) / dpi))
.append('"');
builder.append(" (").append(decimalFormat.format(bounds.getMinX()))
.append(", ").append(decimalFormat.format(bounds.getMinY()))
.append(", ").append(decimalFormat.format(bounds.getMaxX()))
.append(", ").append(decimalFormat.format(bounds.getMaxY()))
.append(")");
}
private static ButtonType showAlert(AlertType alertType, String contentText, ButtonType... buttons) {
return new Alert(alertType, contentText, buttons).showAndWait().orElse(null);
}
@@ -25,20 +25,20 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.mod.ModAdviser;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemInfo;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
@@ -55,8 +55,8 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class HMCLGameRepository extends DefaultGameRepository {
private final Profile profile;
@@ -338,7 +338,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
GameVersionNumber versionNumber = GameVersionNumber.asGameVersion(gameVersion);
if (versionNumber.isAprilFools()) {
return VersionIconType.APRIL_FOOLS.getIcon();
} else if (versionNumber instanceof GameVersionNumber.Snapshot) {
} else if (versionNumber instanceof GameVersionNumber.LegacySnapshot) {
return VersionIconType.COMMAND.getIcon();
} else if (versionNumber instanceof GameVersionNumber.Old) {
return VersionIconType.CRAFT_TABLE.getIcon();
@@ -387,7 +387,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
vs.setUsesGlobal(true);
}
public LaunchOptions getLaunchOptions(String version, JavaRuntime javaVersion, Path gameDir, List<String> javaAgents, List<String> javaArguments, boolean makeLaunchScript) {
public LaunchOptions.Builder getLaunchOptions(String version, JavaRuntime javaVersion, Path gameDir, List<String> javaAgents, List<String> javaArguments, boolean makeLaunchScript) {
VersionSetting vs = getVersionSetting(version);
LaunchOptions.Builder builder = new LaunchOptions.Builder()
@@ -418,7 +418,6 @@ public final class HMCLGameRepository extends DefaultGameRepository {
.setWidth(vs.getWidth())
.setHeight(vs.getHeight())
.setFullscreen(vs.isFullscreen())
.setServerIp(vs.getServerIp())
.setWrapper(vs.getWrapper())
.setPreLaunchCommand(vs.getPreLaunchCommand())
.setPostExitCommand(vs.getPostExitCommand())
@@ -428,12 +427,17 @@ public final class HMCLGameRepository extends DefaultGameRepository {
.setNativesDir(vs.getNativesDir())
.setProcessPriority(vs.getProcessPriority())
.setRenderer(vs.getRenderer())
.setEnableDebugLogOutput(vs.isEnableDebugLogOutput())
.setUseNativeGLFW(vs.isUseNativeGLFW())
.setUseNativeOpenAL(vs.isUseNativeOpenAL())
.setDaemon(!makeLaunchScript && vs.getLauncherVisibility().isDaemon())
.setJavaAgents(javaAgents)
.setJavaArguments(javaArguments);
if (StringUtils.isNotBlank(vs.getServerIp())) {
builder.setQuickPlayOption(new QuickPlayOption.MultiPlayer(vs.getServerIp()));
}
if (config().hasProxy()) {
builder.setProxyType(config().getProxyType());
builder.setProxyHost(config().getProxyHost());
@@ -460,7 +464,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
if (vs.isAutoMemory() && builder.getJavaArguments().stream().anyMatch(it -> it.startsWith("-Xmx")))
builder.setMaxMemory(null);
return builder.create();
return builder;
}
@Override
@@ -22,6 +22,7 @@ import javafx.stage.Stage;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
@@ -51,6 +52,7 @@ import org.jackhuang.hmcl.util.versioning.VersionNumber;
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;
@@ -60,7 +62,6 @@ import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import java.lang.ref.WeakReference;
import static javafx.application.Platform.runLater;
import static javafx.application.Platform.setImplicitExit;
@@ -81,6 +82,8 @@ public final class LauncherHelper {
private final VersionSetting setting;
private LauncherVisibility launcherVisibility;
private boolean showLogs;
private QuickPlayOption quickPlayOption;
private boolean disableOfflineSkin = false;
public LauncherHelper(Profile profile, Account account, String selectedVersion) {
this.profile = Objects.requireNonNull(profile);
@@ -111,6 +114,14 @@ public final class LauncherHelper {
launcherVisibility = LauncherVisibility.KEEP;
}
public void setQuickPlayOption(QuickPlayOption quickPlayOption) {
this.quickPlayOption = quickPlayOption;
}
public void setDisableOfflineSkin() {
disableOfflineSkin = true;
}
public void launch() {
FXUtils.checkFxUserThread();
@@ -188,8 +199,15 @@ public final class LauncherHelper {
.thenComposeAsync(() -> gameVersion.map(s -> new GameVerificationFixTask(dependencyManager, s, version.get())).orElse(null))
.thenComposeAsync(() -> logIn(account).withStage("launch.state.logging_in"))
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
LaunchOptions launchOptions = repository.getLaunchOptions(
LaunchOptions.Builder launchOptionsBuilder = repository.getLaunchOptions(
selectedVersion, javaVersionRef.get(), profile.getGameDir(), javaAgents, javaArguments, scriptFile != null);
if (disableOfflineSkin) {
launchOptionsBuilder.setDaemon(false);
}
if (quickPlayOption != null) {
launchOptionsBuilder.setQuickPlayOption(quickPlayOption);
}
LaunchOptions launchOptions = launchOptionsBuilder.create();
LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModsDirectory(selectedVersion), 10));
@@ -639,10 +657,13 @@ public final class LauncherHelper {
return future;
}
private static Task<AuthInfo> logIn(Account account) {
private Task<AuthInfo> logIn(Account account) {
return Task.composeAsync(() -> {
try {
return Task.completed(account.logIn());
if (disableOfflineSkin && account instanceof OfflineAccount offlineAccount)
return Task.completed(offlineAccount.logInWithoutSkin());
else
return Task.completed(account.logIn());
} catch (CredentialExpiredException e) {
LOG.info("Credential has expired", e);
@@ -47,6 +47,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class StyleSheets {
private static final int FONT_STYLE_SHEET_INDEX = 0;
private static final int THEME_STYLE_SHEET_INDEX = 1;
private static final int BRIGHTNESS_SHEET_INDEX = 2;
private static final ObservableList<String> stylesheets;
@@ -54,12 +55,16 @@ public final class StyleSheets {
String[] array = new String[]{
getFontStyleSheet(),
getThemeStyleSheet(),
getBrightnessStyleSheet(),
"/assets/css/root.css"
};
stylesheets = FXCollections.observableList(Arrays.asList(array));
FontManager.fontProperty().addListener(o -> stylesheets.set(FONT_STYLE_SHEET_INDEX, getFontStyleSheet()));
Themes.colorSchemeProperty().addListener(o -> stylesheets.set(THEME_STYLE_SHEET_INDEX, getThemeStyleSheet()));
Themes.colorSchemeProperty().addListener(o -> {
stylesheets.set(THEME_STYLE_SHEET_INDEX, getThemeStyleSheet());
stylesheets.set(BRIGHTNESS_SHEET_INDEX, getBrightnessStyleSheet());
});
}
private static String toStyleSheetUri(String styleSheet, String fallback) {
@@ -131,6 +136,12 @@ public final class StyleSheets {
return toStyleSheetUri(builder.toString(), defaultCss);
}
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");
@@ -170,13 +181,7 @@ public final class StyleSheets {
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);
// If we use -monet-error-container as the tag color, there may be a conflict between the tag and background colors on the mod management page
if (scheme.getBrightness() == Brightness.LIGHT) {
addColor(builder, "-warning-tag-background", Color.web("#f1aeb5"));
} else {
addColor(builder, "-warning-tag-background", Color.web("#2c0b0e"));
}
addColor(builder, scheme, ColorRole.INVERSE_SURFACE, 0.8);
builder.append("}\n");
return toStyleSheetUri(builder.toString(), blueCss);
@@ -459,6 +459,20 @@ public final class VersionSetting implements Cloneable, Observable {
showLogsProperty.set(showLogs);
}
private final BooleanProperty enableDebugLogOutputProperty = new SimpleBooleanProperty(this, "enableDebugLogOutput", false);
public BooleanProperty enableDebugLogOutputProperty() {
return enableDebugLogOutputProperty;
}
public boolean isEnableDebugLogOutput() {
return enableDebugLogOutputProperty.get();
}
public void setEnableDebugLogOutput(boolean u) {
this.enableDebugLogOutputProperty.set(u);
}
// Minecraft settings.
private final StringProperty serverIpProperty = new SimpleStringProperty(this, "serverIp", "");
@@ -776,6 +790,7 @@ public final class VersionSetting implements Cloneable, Observable {
obj.addProperty("notCheckJVM", src.isNotCheckJVM());
obj.addProperty("notPatchNatives", src.isNotPatchNatives());
obj.addProperty("showLogs", src.isShowLogs());
obj.addProperty("enableDebugLogOutput", src.isEnableDebugLogOutput());
obj.addProperty("gameDir", src.getGameDir());
obj.addProperty("launcherVisibility", src.getLauncherVisibility().ordinal());
obj.addProperty("processPriority", src.getProcessPriority().ordinal());
@@ -847,6 +862,7 @@ public final class VersionSetting implements Cloneable, Observable {
vs.setNotCheckJVM(Optional.ofNullable(obj.get("notCheckJVM")).map(JsonElement::getAsBoolean).orElse(false));
vs.setNotPatchNatives(Optional.ofNullable(obj.get("notPatchNatives")).map(JsonElement::getAsBoolean).orElse(false));
vs.setShowLogs(Optional.ofNullable(obj.get("showLogs")).map(JsonElement::getAsBoolean).orElse(false));
vs.setEnableDebugLogOutput(Optional.ofNullable(obj.get("enableDebugLogOutput")).map(JsonElement::getAsBoolean).orElse(false));
vs.setLauncherVisibility(parseJsonPrimitive(obj.getAsJsonPrimitive("launcherVisibility"), LauncherVisibility.class, LauncherVisibility.HIDE));
vs.setProcessPriority(parseJsonPrimitive(obj.getAsJsonPrimitive("processPriority"), ProcessPriority.class, ProcessPriority.NORMAL));
vs.setUseNativeGLFW(Optional.ofNullable(obj.get("useNativeGLFW")).map(JsonElement::getAsBoolean).orElse(false));
@@ -113,8 +113,16 @@ public final class Themes {
return colorScheme.get();
}
private static final ObjectBinding<Color> titleFill = Bindings.createObjectBinding(
() -> config().isTitleTransparent()
? getColorScheme().getOnSurface()
: getColorScheme().getOnPrimaryContainer(),
colorSchemeProperty(),
config().titleTransparentProperty()
);
public static ObservableValue<Color> titleFillProperty() {
return colorSchemeProperty().getOnPrimaryContainer();
return titleFill;
}
public static BooleanBinding darkModeProperty() {
@@ -1188,7 +1188,14 @@ public final class FXUtils {
public static Task<Image> getRemoteImageTask(String url, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
return new CacheFileTask(url)
.thenApplyAsync(file -> loadImage(file, requestedWidth, requestedHeight, preserveRatio, smooth));
.thenApplyAsync(file -> loadImage(file, requestedWidth, requestedHeight, preserveRatio, smooth))
.setSignificance(Task.TaskSignificance.MINOR);
}
public static Task<Image> getRemoteImageTask(URI uri, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
return new CacheFileTask(uri)
.thenApplyAsync(file -> loadImage(file, requestedWidth, requestedHeight, preserveRatio, smooth))
.setSignificance(Task.TaskSignificance.MINOR);
}
public static ObservableValue<Image> newRemoteImage(String url, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
@@ -1468,7 +1475,6 @@ public final class FXUtils {
hyperlinkAction.accept(link);
});
text.setCursor(Cursor.HAND);
text.setFill(Color.web("#0070E0"));
text.setUnderline(true);
texts.add(text);
} else if ("b".equals(element.getTagName())) {
@@ -400,6 +400,8 @@ public class GameCrashWindow extends Stage {
reasonPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
reasonPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
feedbackTextFlow.getStyleClass().add("crash-reason-text-flow");
gameDirPane.setPadding(new Insets(8));
VBox.setVgrow(gameDirPane, Priority.ALWAYS);
FXUtils.onChangeAndOperate(feedbackTextFlow.visibleProperty(), visible -> {
@@ -21,6 +21,7 @@ import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXListView;
import com.sun.jna.Pointer;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
@@ -28,6 +29,7 @@ import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
@@ -37,17 +39,21 @@ import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import org.jackhuang.hmcl.game.GameDumpGenerator;
import org.jackhuang.hmcl.game.Log;
import org.jackhuang.hmcl.setting.StyleSheets;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.CircularArrayList;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.platform.windows.Dwmapi;
import org.jackhuang.hmcl.util.platform.windows.WinConstants;
import org.jackhuang.hmcl.util.platform.windows.WinTypes;
import java.io.IOException;
import java.nio.file.Files;
@@ -84,6 +90,36 @@ public final class LogWindow extends Stage {
private final LogWindowImpl impl;
private final ManagedProcess gameProcess;
@SuppressWarnings("unused")
private Object windowsDarkModeListenerHolder;
{
if (OperatingSystem.SYSTEM_VERSION.isAtLeast(OSVersion.WINDOWS_11) && NativeUtils.USE_JNA && Dwmapi.INSTANCE != null) {
this.addEventFilter(WindowEvent.WINDOW_SHOWN, new EventHandler<>() {
@Override
public void handle(WindowEvent event) {
LogWindow.this.removeEventFilter(WindowEvent.WINDOW_SHOWN, this);
windowsDarkModeListenerHolder = FXUtils.onWeakChangeAndOperate(Themes.darkModeProperty(), darkMode -> {
if (LogWindow.this.isShowing()) {
WindowsNativeUtils.getWindowHandle(LogWindow.this).ifPresent(handle -> {
if (handle == WinTypes.HANDLE.INVALID_VALUE)
return;
Dwmapi.INSTANCE.DwmSetWindowAttribute(
new WinTypes.HANDLE(Pointer.createConstant(handle)),
WinConstants.DWMWA_USE_IMMERSIVE_DARK_MODE,
new WinTypes.BOOLByReference(new WinTypes.BOOL(darkMode)),
WinTypes.BOOL.SIZE
);
});
}
});
}
});
}
}
public LogWindow(ManagedProcess gameProcess) {
this(gameProcess, new CircularArrayList<>());
}
@@ -264,7 +300,6 @@ public final class LogWindow extends Stage {
VBox vbox = new VBox(3);
vbox.setPadding(new Insets(3, 0, 3, 0));
vbox.setStyle("-fx-background-color: white");
getChildren().setAll(vbox);
{
@@ -286,8 +321,7 @@ public final class LogWindow extends Stage {
HBox hBox = new HBox(3);
for (int i = 0; i < LEVELS.length; i++) {
ToggleButton button = new ToggleButton();
button.setStyle("-fx-background-color: " + FXUtils.toWeb(LEVELS[i].getColor()) + ";");
button.getStyleClass().add("log-toggle");
button.getStyleClass().addAll("log-toggle", LEVELS[i].name().toLowerCase(Locale.ROOT));
button.textProperty().bind(control.buttonText[i]);
button.setSelected(true);
control.showLevel[i].bind(button.selectedProperty());
@@ -19,8 +19,8 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXSpinner;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.ScrollPane;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.task.Schedulers;
@@ -48,7 +48,7 @@ public final class UpgradeDialog extends JFXDialogLayout {
maxHeightProperty().bind(Controllers.getScene().heightProperty().multiply(0.7));
setHeading(new Label(i18n("update.changelog")));
setBody(new ProgressIndicator());
setBody(new JFXSpinner());
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html";
@@ -0,0 +1,60 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import javafx.stage.Stage;
import javafx.stage.Window;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.OptionalLong;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/// @author Glavo
public final class WindowsNativeUtils {
public static OptionalLong getWindowHandle(Stage stage) {
try {
Class<?> windowStageClass = Class.forName("com.sun.javafx.tk.quantum.WindowStage");
Class<?> glassWindowClass = Class.forName("com.sun.glass.ui.Window");
Class<?> tkStageClass = Class.forName("com.sun.javafx.tk.TKStage");
Object tkStage = MethodHandles.privateLookupIn(Window.class, MethodHandles.lookup())
.findVirtual(Window.class, "getPeer", MethodType.methodType(tkStageClass))
.invoke(stage);
MethodHandles.Lookup windowStageLookup = MethodHandles.privateLookupIn(windowStageClass, MethodHandles.lookup());
MethodHandle getPlatformWindow = windowStageLookup.findVirtual(windowStageClass, "getPlatformWindow", MethodType.methodType(glassWindowClass));
Object platformWindow = getPlatformWindow.invoke(tkStage);
long handle = (long) MethodHandles.privateLookupIn(glassWindowClass, MethodHandles.lookup())
.findVirtual(glassWindowClass, "getNativeWindow", MethodType.methodType(long.class))
.invoke(platformWindow);
return OptionalLong.of(handle);
} catch (Throwable ex) {
LOG.warning("Failed to get window handle", ex);
return OptionalLong.empty();
}
}
private WindowsNativeUtils() {
}
}
@@ -68,6 +68,10 @@ public class AccountAdvancedListItem extends AdvancedListItem {
};
public AccountAdvancedListItem() {
this(null);
}
public AccountAdvancedListItem(Account account) {
tooltip = new Tooltip();
FXUtils.installFastTooltip(this, tooltip);
@@ -76,9 +80,13 @@ public class AccountAdvancedListItem extends AdvancedListItem {
setActionButtonVisible(false);
FXUtils.onScroll(this, Accounts.getAccounts(),
accounts -> accounts.indexOf(account.get()),
Accounts::setSelectedAccount);
if (account != null) {
this.accountProperty().set(account);
} else {
FXUtils.onScroll(this, Accounts.getAccounts(),
accounts -> accounts.indexOf(accountProperty().get()),
Accounts::setSelectedAccount);
}
}
public ObjectProperty<Account> accountProperty() {
@@ -28,7 +28,6 @@ import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import java.util.Locale;
@@ -45,29 +44,9 @@ public class HintPane extends VBox {
setFillWidth(true);
getStyleClass().addAll("hint", type.name().toLowerCase(Locale.ROOT));
SVG svg;
switch (type) {
case INFO:
svg = SVG.INFO;
break;
case ERROR:
svg = SVG.ERROR;
break;
case SUCCESS:
svg = SVG.CHECK_CIRCLE;
break;
case WARNING:
svg = SVG.WARNING;
break;
case QUESTION:
svg = SVG.HELP;
break;
default:
throw new IllegalArgumentException("Unrecognized message box message type " + type);
}
HBox hbox = new HBox(svg.createIcon(16), new Text(type.getDisplayName()));
HBox hbox = new HBox(type.getIcon().createIcon(16), new Text(type.getDisplayName()));
hbox.setAlignment(Pos.CENTER_LEFT);
hbox.setSpacing(2);
flow.getChildren().setAll(label);
getChildren().setAll(hbox, flow);
label.textProperty().bind(text);
@@ -24,7 +24,7 @@ import org.jackhuang.hmcl.ui.SVG;
public final class IconedMenuItem extends IconedItem {
public IconedMenuItem(SVG icon, String text, Runnable action, JFXPopup popup) {
super(icon != null ? icon.createIcon(14) : null, text);
super(icon != null ? FXUtils.limitingSize(icon.createIcon(14), 14, 14) : null, text);
getStyleClass().setAll("iconed-menu-item");
@@ -0,0 +1,68 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXCheckBox;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.util.Callback;
/// @author Glavo
public final class JFXCheckBoxTableCell<S, T> extends TableCell<S, T> {
public static <S> Callback<TableColumn<S, Boolean>, TableCell<S, Boolean>> forTableColumn(
final TableColumn<S, Boolean> column) {
return list -> new JFXCheckBoxTableCell<>();
}
private final JFXCheckBox checkBox = new JFXCheckBox();
private BooleanProperty booleanProperty;
public JFXCheckBoxTableCell() {
this.getStyleClass().add("jfx-checkbox-table-cell");
}
@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
checkBox.disableProperty().unbind();
} else {
setGraphic(checkBox);
if (booleanProperty != null) {
checkBox.selectedProperty().unbindBidirectional(booleanProperty);
}
if (getTableColumn().getCellObservableValue(getIndex()) instanceof BooleanProperty obsValue) {
booleanProperty = obsValue;
checkBox.selectedProperty().bindBidirectional(booleanProperty);
}
checkBox.disableProperty().bind(Bindings.not(
getTableView().editableProperty().and(
getTableColumn().editableProperty()).and(
editableProperty())
));
}
}
}
@@ -46,11 +46,21 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class MessageDialogPane extends HBox {
public enum MessageType {
ERROR,
INFO,
WARNING,
QUESTION,
SUCCESS;
ERROR(SVG.ERROR),
INFO(SVG.INFO),
WARNING(SVG.WARNING),
QUESTION(SVG.HELP),
SUCCESS(SVG.CHECK_CIRCLE);
private final SVG icon;
MessageType(SVG icon) {
this.icon = icon;
}
public SVG getIcon() {
return icon;
}
public String getDisplayName() {
return i18n("message." + name().toLowerCase(Locale.ROOT));
@@ -70,27 +80,7 @@ public final class MessageDialogPane extends HBox {
graphic.setTranslateY(10);
graphic.setMinSize(40, 40);
graphic.setMaxSize(40, 40);
SVG svg;
switch (type) {
case INFO:
svg = SVG.INFO;
break;
case ERROR:
svg = SVG.ERROR;
break;
case SUCCESS:
svg = SVG.CHECK_CIRCLE;
break;
case WARNING:
svg = SVG.WARNING;
break;
case QUESTION:
svg = SVG.HELP;
break;
default:
throw new IllegalArgumentException("Unrecognized message box message type " + type);
}
graphic.setGraphic(svg.createIcon(40));
graphic.setGraphic(type.getIcon().createIcon(40));
VBox vbox = new VBox();
HBox.setHgrow(vbox, Priority.ALWAYS);
@@ -51,8 +51,9 @@ public class OptionToggleButton extends StackPane {
Label titleLabel = new Label();
titleLabel.textProperty().bind(title);
Label subtitleLabel = new Label();
subtitleLabel.setMouseTransparent(true);
subtitleLabel.setWrapText(true);
subtitleLabel.setMouseTransparent(true);
subtitleLabel.getStyleClass().add("subtitle");
subtitleLabel.textProperty().bind(subtitle);
pane.setCenter(left);
left.setAlignment(Pos.CENTER_LEFT);
@@ -373,9 +373,8 @@ public class DecoratorController {
if (navigator.getCurrentPage() instanceof DecoratorPage) {
DecoratorPage page = (DecoratorPage) navigator.getCurrentPage();
// FIXME: Get WorldPage working first, and revisit this later
page.closePage();
if (page.isPageCloseable()) {
page.closePage();
return;
}
}
@@ -304,6 +304,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
BorderPane center = new BorderPane();
if (title != null) {
Label titleLabel = new Label();
titleLabel.textFillProperty().bind(Themes.titleFillProperty());
BorderPane.setAlignment(titleLabel, Pos.CENTER_LEFT);
titleLabel.getStyleClass().add("jfx-decorator-title");
if (titleNode == null) {
@@ -200,6 +200,10 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
tab.select(modpackTab, false);
}
public void showResourcepackDownloads() {
tab.select(resourcePackTab, false);
}
public DownloadListPage showModDownloads() {
tab.select(modTab, false);
return modTab.getNode();
@@ -233,7 +233,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
}
setGraphic(pane);
twoLineListItem.setTitle(I18n.getDisplaySelfVersion(remoteVersion));
twoLineListItem.setTitle(I18n.getDisplayVersion(remoteVersion));
if (remoteVersion.getReleaseDate() != null) {
twoLineListItem.setSubtitle(I18n.formatDateTime(remoteVersion.getReleaseDate()));
} else {
@@ -43,34 +43,41 @@ public class FeedbackPage extends SpinnerPane {
FXUtils.smoothScrolling(scrollPane);
setContent(scrollPane);
ComponentList community = new ComponentList();
ComponentList groups = new ComponentList();
{
IconedTwoLineListItem users = new IconedTwoLineListItem();
users.setImage(FXUtils.newBuiltinImage("/assets/img/icon.png"));
users.setTitle(i18n("feedback.qq_group"));
users.setSubtitle(i18n("feedback.qq_group.statement"));
users.setTitle(i18n("contact.chat.qq_group"));
users.setSubtitle(i18n("contact.chat.qq_group.statement"));
users.setExternalLink(Metadata.GROUPS_URL);
IconedTwoLineListItem discord = new IconedTwoLineListItem();
discord.setImage(FXUtils.newBuiltinImage("/assets/img/discord.png"));
discord.setTitle(i18n("contact.chat.discord"));
discord.setSubtitle(i18n("contact.chat.discord.statement"));
discord.setExternalLink("https://discord.gg/jVvC7HfM6U");
groups.getContent().setAll(users, discord);
}
ComponentList feedback = new ComponentList();
{
IconedTwoLineListItem github = new IconedTwoLineListItem();
github.imageProperty().bind(Bindings.when(Themes.darkModeProperty())
.then(FXUtils.newBuiltinImage("/assets/img/github-white.png"))
.otherwise(FXUtils.newBuiltinImage("/assets/img/github.png")));
github.setTitle(i18n("feedback.github"));
github.setSubtitle(i18n("feedback.github.statement"));
github.setTitle(i18n("contact.feedback.github"));
github.setSubtitle(i18n("contact.feedback.github.statement"));
github.setExternalLink("https://github.com/HMCL-dev/HMCL/issues/new/choose");
IconedTwoLineListItem discord = new IconedTwoLineListItem();
discord.setImage(FXUtils.newBuiltinImage("/assets/img/discord.png"));
discord.setTitle(i18n("feedback.discord"));
discord.setSubtitle(i18n("feedback.discord.statement"));
discord.setExternalLink("https://discord.gg/jVvC7HfM6U");
community.getContent().setAll(users, github, discord);
feedback.getContent().setAll(github);
}
content.getChildren().addAll(
ComponentList.createComponentListTitle(i18n("feedback.channel")),
community
ComponentList.createComponentListTitle(i18n("contact.chat")),
groups,
ComponentList.createComponentListTitle(i18n("contact.feedback")),
feedback
);
this.setContent(content);
@@ -69,7 +69,7 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor
.addNavigationDrawerTab(tab, downloadTab, i18n("download"), SVG.DOWNLOAD)
.startCategory(i18n("help").toUpperCase(Locale.ROOT))
.addNavigationDrawerTab(tab, helpTab, i18n("help"), SVG.HELP, SVG.HELP_FILL)
.addNavigationDrawerTab(tab, feedbackTab, i18n("feedback"), SVG.FEEDBACK, SVG.FEEDBACK_FILL)
.addNavigationDrawerTab(tab, feedbackTab, i18n("contact"), SVG.FEEDBACK, SVG.FEEDBACK_FILL)
.addNavigationDrawerTab(tab, aboutTab, i18n("about"), SVG.INFO, SVG.INFO_FILL);
FXUtils.setLimitWidth(sideBar, 200);
setLeft(sideBar);
@@ -52,6 +52,7 @@ import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
@@ -120,6 +121,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
titleLabel.setRotate(180);
}
titleLabel.getStyleClass().add("jfx-decorator-title");
titleLabel.textFillProperty().bind(Themes.titleFillProperty());
titleNode.getChildren().setAll(titleIcon, titleLabel);
state.setValue(new State(null, titleNode, false, false, true));
@@ -186,16 +188,13 @@ public final class MainPage extends StackPane implements DecoratorPage {
StackPane.setAlignment(hBox, Pos.CENTER_LEFT);
StackPane.setMargin(hBox, new Insets(9, 12, 9, 16));
{
Label lblIcon = new Label();
lblIcon.setGraphic(SVG.UPDATE.createIcon(20));
TwoLineListItem prompt = new TwoLineListItem();
prompt.setSubtitle(i18n("update.bubble.subtitle"));
prompt.setPickOnBounds(false);
prompt.titleProperty().bind(BindingMapping.of(latestVersionProperty()).map(latestVersion ->
latestVersion == null ? "" : i18n("update.bubble.title", latestVersion.getVersion())));
hBox.getChildren().setAll(lblIcon, prompt);
hBox.getChildren().setAll(SVG.UPDATE.createIcon(20), prompt);
}
JFXButton closeUpdateButton = new JFXButton();
@@ -347,7 +346,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
private void launch() {
Profile profile = Profiles.getSelectedProfile();
Versions.launch(profile, profile.getSelectedVersion(), null);
Versions.launch(profile, profile.getSelectedVersion());
}
private void launchNoGame() {
@@ -44,6 +44,7 @@ import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
@@ -86,8 +87,11 @@ public class PersonalizationPage extends StackPane {
brightnessPane.setLeft(left);
JFXComboBox<String> cboBrightness = new JFXComboBox<>(
FXCollections.observableArrayList("auto", "light", "dark"));
JFXComboBox<String> cboBrightness = new JFXComboBox<>(FXCollections.observableArrayList(
FXUtils.DARK_MODE != null
? List.of("auto", "light", "dark")
: List.of("light", "dark")
));
cboBrightness.setConverter(FXUtils.stringConverter(name -> i18n("settings.launcher.brightness." + name)));
cboBrightness.valueProperty().bindBidirectional(config().themeBrightnessProperty());
brightnessPane.setRight(cboBrightness);
@@ -17,9 +17,11 @@
*/
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXPopup;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.scene.input.MouseButton;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.game.HMCLGameRepository;
@@ -39,6 +41,7 @@ import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.PopupMenu;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
@@ -64,8 +67,8 @@ import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
private MainPage mainPage = null;
@@ -144,6 +147,13 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
// first item in left sidebar
AccountAdvancedListItem accountListItem = new AccountAdvancedListItem();
accountListItem.setOnAction(e -> Controllers.navigate(Controllers.getAccountListPage()));
accountListItem.setOnMouseClicked(e -> {
if (e.getButton() == MouseButton.SECONDARY) {
if (!Accounts.getAccounts().isEmpty())
showAccountListPopupMenu(accountListItem);
e.consume();
}
});
accountListItem.accountProperty().bind(Accounts.selectedAccountProperty());
// second item in left sidebar
@@ -234,13 +244,39 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
.startCategory(i18n("settings.launcher.general").toUpperCase(Locale.ROOT))
.add(launcherSettingsItem)
.add(terracottaItem)
.addNavigationDrawerItem(i18n("chat"), SVG.CHAT, () -> FXUtils.openLink(Metadata.GROUPS_URL));
.addNavigationDrawerItem(i18n("contact.chat"), SVG.CHAT, () -> {
Controllers.getSettingsPage().showFeedback();
Controllers.navigate(Controllers.getSettingsPage());
});
// the root page, with the sidebar in left, navigator in center.
setLeft(sideBar);
setCenter(getSkinnable().getMainPage());
}
public void showAccountListPopupMenu(
AccountAdvancedListItem accountListItem
) {
PopupMenu popupMenu = new PopupMenu();
JFXPopup popup = new JFXPopup(popupMenu);
AdvancedListBox scrollPane = new AdvancedListBox();
scrollPane.getStyleClass().add("no-padding");
scrollPane.setPrefWidth(220);
scrollPane.setPrefHeight(-1);
scrollPane.setMaxHeight(260);
for (Account account : Accounts.getAccounts()) {
AccountAdvancedListItem item = new AccountAdvancedListItem(account);
item.setOnAction(e -> {
Accounts.setSelectedAccount(account);
popup.hide();
});
scrollPane.add(item);
}
popupMenu.getContent().add(scrollPane);
popup.show(accountListItem, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, accountListItem.getWidth(), 0);
}
}
private boolean checkedModpack = false;
@@ -17,22 +17,40 @@
*/
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXRadioButton;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Cursor;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.*;
import javafx.scene.text.TextAlignment;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.ComponentSublist;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.MultiFileItem;
import org.jackhuang.hmcl.ui.construct.OptionToggleButton;
import org.jackhuang.hmcl.upgrade.RemoteVersion;
import org.jackhuang.hmcl.upgrade.UpdateChannel;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.IOUtils;
@@ -45,6 +63,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.zip.GZIPInputStream;
@@ -52,79 +71,254 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class SettingsPage extends SettingsView {
public final class SettingsPage extends ScrollPane {
private InvalidationListener updateListener;
@SuppressWarnings("FieldCanBeLocal")
private final ToggleGroup updateChannelGroup;
@SuppressWarnings("FieldCanBeLocal")
private final InvalidationListener updateListener;
public SettingsPage() {
FXUtils.smoothScrolling(scroll);
this.setFitToWidth(true);
// ==== Languages ====
cboLanguage.getItems().setAll(SupportedLocale.getSupportedLocales());
selectedItemPropertyFor(cboLanguage).bindBidirectional(config().localizationProperty());
VBox rootPane = new VBox();
rootPane.setPadding(new Insets(32, 10, 32, 10));
this.setContent(rootPane);
FXUtils.smoothScrolling(this);
disableAutoGameOptionsPane.selectedProperty().bindBidirectional(config().disableAutoGameOptionsProperty());
// ====
ComponentList settingsPane = new ComponentList();
{
{
StackPane sponsorPane = new StackPane();
sponsorPane.setCursor(Cursor.HAND);
FXUtils.onClicked(sponsorPane, this::onSponsor);
sponsorPane.setPadding(new Insets(8, 0, 8, 0));
fileCommonLocation.selectedDataProperty().bindBidirectional(config().commonDirTypeProperty());
fileCommonLocationSublist.subtitleProperty().bind(
Bindings.createObjectBinding(() -> Optional.ofNullable(Settings.instance().getCommonDirectory())
.orElse(i18n("launcher.cache_directory.disabled")),
config().commonDirectoryProperty(), config().commonDirTypeProperty()));
GridPane gridPane = new GridPane();
// ==== Update ====
FXUtils.installFastTooltip(btnUpdate, i18n("update.tooltip"));
updateListener = any -> {
btnUpdate.setVisible(UpdateChecker.isOutdated());
ColumnConstraints col = new ColumnConstraints();
col.setHgrow(Priority.SOMETIMES);
col.setMaxWidth(Double.POSITIVE_INFINITY);
if (UpdateChecker.isOutdated()) {
lblUpdateSub.setText(i18n("update.newest_version", UpdateChecker.getLatestVersion().getVersion()));
lblUpdateSub.getStyleClass().setAll("update-label");
gridPane.getColumnConstraints().setAll(col);
lblUpdate.setText(i18n("update.found"));
lblUpdate.getStyleClass().setAll("update-label");
} else if (UpdateChecker.isCheckingUpdate()) {
lblUpdateSub.setText(i18n("update.checking"));
lblUpdateSub.getStyleClass().setAll("subtitle-label");
RowConstraints row = new RowConstraints();
row.setMinHeight(Double.NEGATIVE_INFINITY);
row.setValignment(VPos.TOP);
row.setVgrow(Priority.SOMETIMES);
gridPane.getRowConstraints().setAll(row);
lblUpdate.setText(i18n("update"));
lblUpdate.getStyleClass().setAll("title-label");
} else {
lblUpdateSub.setText(i18n("update.latest"));
lblUpdateSub.getStyleClass().setAll("subtitle-label");
{
Label label = new Label(i18n("sponsor.hmcl"));
label.setWrapText(true);
label.setTextAlignment(TextAlignment.JUSTIFY);
GridPane.setRowIndex(label, 0);
GridPane.setColumnIndex(label, 0);
gridPane.getChildren().add(label);
}
lblUpdate.setText(i18n("update"));
lblUpdate.getStyleClass().setAll("title-label");
sponsorPane.getChildren().setAll(gridPane);
settingsPane.getContent().add(sponsorPane);
}
};
UpdateChecker.latestVersionProperty().addListener(new WeakInvalidationListener(updateListener));
UpdateChecker.outdatedProperty().addListener(new WeakInvalidationListener(updateListener));
UpdateChecker.checkingUpdateProperty().addListener(new WeakInvalidationListener(updateListener));
updateListener.invalidated(null);
ToggleGroup updateChannelGroup = new ToggleGroup();
chkUpdateDev.setToggleGroup(updateChannelGroup);
chkUpdateDev.setUserData(UpdateChannel.DEVELOPMENT);
chkUpdateStable.setToggleGroup(updateChannelGroup);
chkUpdateStable.setUserData(UpdateChannel.STABLE);
ObjectProperty<UpdateChannel> updateChannel = selectedItemPropertyFor(updateChannelGroup, UpdateChannel.class);
updateChannel.set(UpdateChannel.getChannel());
{
ComponentSublist updatePane = new ComponentSublist();
updatePane.setTitle(i18n("update"));
updatePane.setHasSubtitle(true);
InvalidationListener checkUpdateListener = e -> {
UpdateChecker.requestCheckUpdate(updateChannel.get(), previewPane.isSelected());
};
updateChannel.addListener(checkUpdateListener);
previewPane.selectedProperty().addListener(checkUpdateListener);
// ====
final Label lblUpdate;
final Label lblUpdateSub;
{
VBox headerLeft = new VBox();
lblUpdate = new Label(i18n("update"));
lblUpdate.getStyleClass().add("title-label");
lblUpdateSub = new Label();
lblUpdateSub.getStyleClass().add("subtitle-label");
headerLeft.getChildren().setAll(lblUpdate, lblUpdateSub);
updatePane.setHeaderLeft(headerLeft);
}
{
JFXButton btnUpdate = new JFXButton();
btnUpdate.setOnAction(e -> onUpdate());
btnUpdate.getStyleClass().add("toggle-icon4");
btnUpdate.setGraphic(SVG.UPDATE.createIcon(20));
FXUtils.installFastTooltip(btnUpdate, i18n("update.tooltip"));
updateListener = any -> {
btnUpdate.setVisible(UpdateChecker.isOutdated());
if (UpdateChecker.isOutdated()) {
lblUpdateSub.setText(i18n("update.newest_version", UpdateChecker.getLatestVersion().getVersion()));
lblUpdateSub.getStyleClass().setAll("update-label");
lblUpdate.setText(i18n("update.found"));
lblUpdate.getStyleClass().setAll("update-label");
} else if (UpdateChecker.isCheckingUpdate()) {
lblUpdateSub.setText(i18n("update.checking"));
lblUpdateSub.getStyleClass().setAll("subtitle-label");
lblUpdate.setText(i18n("update"));
lblUpdate.getStyleClass().setAll("title-label");
} else {
lblUpdateSub.setText(i18n("update.latest"));
lblUpdateSub.getStyleClass().setAll("subtitle-label");
lblUpdate.setText(i18n("update"));
lblUpdate.getStyleClass().setAll("title-label");
}
};
UpdateChecker.latestVersionProperty().addListener(new WeakInvalidationListener(updateListener));
UpdateChecker.outdatedProperty().addListener(new WeakInvalidationListener(updateListener));
UpdateChecker.checkingUpdateProperty().addListener(new WeakInvalidationListener(updateListener));
updateListener.invalidated(null);
updatePane.setHeaderRight(btnUpdate);
}
{
VBox content = new VBox();
content.setSpacing(8);
JFXRadioButton chkUpdateStable = new JFXRadioButton(i18n("update.channel.stable"));
JFXRadioButton chkUpdateDev = new JFXRadioButton(i18n("update.channel.dev"));
updateChannelGroup = new ToggleGroup();
chkUpdateDev.setToggleGroup(updateChannelGroup);
chkUpdateDev.setUserData(UpdateChannel.DEVELOPMENT);
chkUpdateStable.setToggleGroup(updateChannelGroup);
chkUpdateStable.setUserData(UpdateChannel.STABLE);
Label noteWrapper = new Label(i18n("update.note"));
VBox.setMargin(noteWrapper, new Insets(10, 0, 0, 0));
content.getChildren().setAll(chkUpdateStable, chkUpdateDev, noteWrapper);
updatePane.getContent().add(content);
}
settingsPane.getContent().add(updatePane);
}
{
OptionToggleButton previewPane = new OptionToggleButton();
previewPane.setTitle(i18n("update.preview"));
previewPane.setSubtitle(i18n("update.preview.subtitle"));
previewPane.selectedProperty().bindBidirectional(config().acceptPreviewUpdateProperty());
ObjectProperty<UpdateChannel> updateChannel = selectedItemPropertyFor(updateChannelGroup, UpdateChannel.class);
updateChannel.set(UpdateChannel.getChannel());
InvalidationListener checkUpdateListener = e -> {
UpdateChecker.requestCheckUpdate(updateChannel.get(), previewPane.isSelected());
};
updateChannel.addListener(checkUpdateListener);
previewPane.selectedProperty().addListener(checkUpdateListener);
settingsPane.getContent().add(previewPane);
}
{
MultiFileItem<EnumCommonDirectory> fileCommonLocation = new MultiFileItem<>();
fileCommonLocation.selectedDataProperty().bindBidirectional(config().commonDirTypeProperty());
fileCommonLocation.loadChildren(Arrays.asList(
new MultiFileItem.Option<>(i18n("launcher.cache_directory.default"), EnumCommonDirectory.DEFAULT),
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumCommonDirectory.CUSTOM)
.setChooserTitle(i18n("launcher.cache_directory.choose"))
.setDirectory(true)
.bindBidirectional(config().commonDirectoryProperty())
));
ComponentSublist fileCommonLocationSublist = new ComponentSublist();
fileCommonLocationSublist.getContent().add(fileCommonLocation);
fileCommonLocationSublist.setTitle(i18n("launcher.cache_directory"));
fileCommonLocationSublist.setHasSubtitle(true);
fileCommonLocationSublist.subtitleProperty().bind(
Bindings.createObjectBinding(() -> Optional.ofNullable(Settings.instance().getCommonDirectory())
.orElse(i18n("launcher.cache_directory.disabled")),
config().commonDirectoryProperty(), config().commonDirTypeProperty()));
JFXButton cleanButton = FXUtils.newBorderButton(i18n("launcher.cache_directory.clean"));
cleanButton.setOnAction(e -> clearCacheDirectory());
fileCommonLocationSublist.setHeaderRight(cleanButton);
settingsPane.getContent().add(fileCommonLocationSublist);
}
{
BorderPane languagePane = new BorderPane();
Label left = new Label(i18n("settings.launcher.language"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
languagePane.setLeft(left);
SupportedLocale currentLocale = I18n.getLocale();
JFXComboBox<SupportedLocale> cboLanguage = new JFXComboBox<>();
cboLanguage.setConverter(stringConverter(locale -> {
if (locale.isDefault())
return locale.getDisplayName(currentLocale);
else if (locale.isSameLanguage(currentLocale))
return locale.getDisplayName(locale);
else
return locale.getDisplayName(currentLocale) + " - " + locale.getDisplayName(locale);
}));
cboLanguage.getItems().setAll(SupportedLocale.getSupportedLocales());
selectedItemPropertyFor(cboLanguage).bindBidirectional(config().localizationProperty());
FXUtils.setLimitWidth(cboLanguage, 300);
languagePane.setRight(cboLanguage);
settingsPane.getContent().add(languagePane);
}
{
OptionToggleButton disableAutoGameOptionsPane = new OptionToggleButton();
disableAutoGameOptionsPane.setTitle(i18n("settings.launcher.disable_auto_game_options"));
disableAutoGameOptionsPane.selectedProperty().bindBidirectional(config().disableAutoGameOptionsProperty());
settingsPane.getContent().add(disableAutoGameOptionsPane);
}
{
BorderPane debugPane = new BorderPane();
Label left = new Label(i18n("settings.launcher.debug"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
debugPane.setLeft(left);
JFXButton openLogFolderButton = new JFXButton(i18n("settings.launcher.launcher_log.reveal"));
openLogFolderButton.setOnAction(e -> openLogFolder());
openLogFolderButton.getStyleClass().add("jfx-button-border");
if (LOG.getLogFile() == null)
openLogFolderButton.setDisable(true);
JFXButton logButton = FXUtils.newBorderButton(i18n("settings.launcher.launcher_log.export"));
logButton.setOnAction(e -> onExportLogs());
HBox buttonBox = new HBox();
buttonBox.setSpacing(10);
buttonBox.getChildren().addAll(openLogFolderButton, logButton);
BorderPane.setAlignment(buttonBox, Pos.CENTER_RIGHT);
debugPane.setRight(buttonBox);
settingsPane.getContent().add(debugPane);
}
rootPane.getChildren().add(settingsPane);
}
}
@Override
protected void onUpdate() {
private void openLogFolder() {
FXUtils.openFolder(LOG.getLogFile().getParent());
}
private void onUpdate() {
RemoteVersion target = UpdateChecker.getLatestVersion();
if (target == null) {
return;
@@ -168,8 +362,7 @@ public final class SettingsPage extends SettingsView {
}
}
@Override
protected void onExportLogs() {
private void onExportLogs() {
thread(() -> {
String nameBase = "hmcl-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss"));
List<Path> recentLogFiles = LOG.findRecentLogFiles(5);
@@ -249,13 +442,11 @@ public final class SettingsPage extends SettingsView {
});
}
@Override
protected void onSponsor() {
private void onSponsor() {
FXUtils.openLink("https://github.com/HMCL-dev/HMCL");
}
@Override
protected void clearCacheDirectory() {
private void clearCacheDirectory() {
String commonDirectory = Settings.instance().getCommonDirectory();
if (commonDirectory != null) {
FileUtils.cleanDirectoryQuietly(Path.of(commonDirectory, "cache"));
@@ -1,255 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXRadioButton;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Cursor;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.*;
import javafx.scene.text.TextAlignment;
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.ComponentSublist;
import org.jackhuang.hmcl.ui.construct.MultiFileItem;
import org.jackhuang.hmcl.ui.construct.OptionToggleButton;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
import java.util.Arrays;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public abstract class SettingsView extends StackPane {
protected final JFXComboBox<SupportedLocale> cboLanguage;
protected final OptionToggleButton disableAutoGameOptionsPane;
protected final MultiFileItem<EnumCommonDirectory> fileCommonLocation;
protected final ComponentSublist fileCommonLocationSublist;
protected final Label lblUpdate;
protected final Label lblUpdateSub;
protected final JFXRadioButton chkUpdateStable;
protected final JFXRadioButton chkUpdateDev;
protected final JFXButton btnUpdate;
protected final OptionToggleButton previewPane;
protected final ScrollPane scroll;
public SettingsView() {
scroll = new ScrollPane();
getChildren().setAll(scroll);
scroll.setStyle("-fx-font-size: 14;");
scroll.setFitToWidth(true);
{
VBox rootPane = new VBox();
rootPane.setPadding(new Insets(32, 10, 32, 10));
{
ComponentList settingsPane = new ComponentList();
{
{
StackPane sponsorPane = new StackPane();
sponsorPane.setCursor(Cursor.HAND);
FXUtils.onClicked(sponsorPane, this::onSponsor);
sponsorPane.setPadding(new Insets(8, 0, 8, 0));
GridPane gridPane = new GridPane();
ColumnConstraints col = new ColumnConstraints();
col.setHgrow(Priority.SOMETIMES);
col.setMaxWidth(Double.POSITIVE_INFINITY);
gridPane.getColumnConstraints().setAll(col);
RowConstraints row = new RowConstraints();
row.setMinHeight(Double.NEGATIVE_INFINITY);
row.setValignment(VPos.TOP);
row.setVgrow(Priority.SOMETIMES);
gridPane.getRowConstraints().setAll(row);
{
Label label = new Label(i18n("sponsor.hmcl"));
label.setWrapText(true);
label.setTextAlignment(TextAlignment.JUSTIFY);
GridPane.setRowIndex(label, 0);
GridPane.setColumnIndex(label, 0);
gridPane.getChildren().add(label);
}
sponsorPane.getChildren().setAll(gridPane);
settingsPane.getContent().add(sponsorPane);
}
}
{
ComponentSublist updatePane = new ComponentSublist();
updatePane.setTitle(i18n("update"));
updatePane.setHasSubtitle(true);
{
VBox headerLeft = new VBox();
lblUpdate = new Label(i18n("update"));
lblUpdate.getStyleClass().add("title-label");
lblUpdateSub = new Label();
lblUpdateSub.getStyleClass().add("subtitle-label");
headerLeft.getChildren().setAll(lblUpdate, lblUpdateSub);
updatePane.setHeaderLeft(headerLeft);
}
{
btnUpdate = new JFXButton();
btnUpdate.setOnAction(e -> onUpdate());
btnUpdate.getStyleClass().add("toggle-icon4");
btnUpdate.setGraphic(SVG.UPDATE.createIcon(20));
updatePane.setHeaderRight(btnUpdate);
}
{
VBox content = new VBox();
content.setSpacing(8);
chkUpdateStable = new JFXRadioButton(i18n("update.channel.stable"));
chkUpdateDev = new JFXRadioButton(i18n("update.channel.dev"));
Label noteWrapper = new Label(i18n("update.note"));
VBox.setMargin(noteWrapper, new Insets(10, 0, 0, 0));
content.getChildren().setAll(chkUpdateStable, chkUpdateDev, noteWrapper);
updatePane.getContent().add(content);
}
settingsPane.getContent().add(updatePane);
}
{
previewPane = new OptionToggleButton();
previewPane.setTitle(i18n("update.preview"));
previewPane.selectedProperty().bindBidirectional(config().acceptPreviewUpdateProperty());
FXUtils.installFastTooltip(previewPane, i18n("update.preview.tooltip"));
settingsPane.getContent().add(previewPane);
}
{
fileCommonLocation = new MultiFileItem<>();
fileCommonLocationSublist = new ComponentSublist();
fileCommonLocationSublist.getContent().add(fileCommonLocation);
fileCommonLocationSublist.setTitle(i18n("launcher.cache_directory"));
fileCommonLocationSublist.setHasSubtitle(true);
fileCommonLocation.loadChildren(Arrays.asList(
new MultiFileItem.Option<>(i18n("launcher.cache_directory.default"), EnumCommonDirectory.DEFAULT),
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumCommonDirectory.CUSTOM)
.setChooserTitle(i18n("launcher.cache_directory.choose"))
.setDirectory(true)
.bindBidirectional(config().commonDirectoryProperty())
));
{
JFXButton cleanButton = FXUtils.newBorderButton(i18n("launcher.cache_directory.clean"));
cleanButton.setOnAction(e -> clearCacheDirectory());
fileCommonLocationSublist.setHeaderRight(cleanButton);
}
settingsPane.getContent().add(fileCommonLocationSublist);
}
{
BorderPane languagePane = new BorderPane();
Label left = new Label(i18n("settings.launcher.language"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
languagePane.setLeft(left);
SupportedLocale currentLocale = I18n.getLocale();
cboLanguage = new JFXComboBox<>();
cboLanguage.setConverter(stringConverter(locale -> {
if (locale.isDefault())
return locale.getDisplayName(currentLocale);
else if (locale.isSameLanguage(currentLocale))
return locale.getDisplayName(locale);
else
return locale.getDisplayName(currentLocale) + " - " + locale.getDisplayName(locale);
}));
FXUtils.setLimitWidth(cboLanguage, 300);
languagePane.setRight(cboLanguage);
settingsPane.getContent().add(languagePane);
}
{
disableAutoGameOptionsPane = new OptionToggleButton();
disableAutoGameOptionsPane.setTitle(i18n("settings.launcher.disable_auto_game_options"));
settingsPane.getContent().add(disableAutoGameOptionsPane);
}
{
BorderPane debugPane = new BorderPane();
Label left = new Label(i18n("settings.launcher.debug"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
debugPane.setLeft(left);
JFXButton openLogFolderButton = new JFXButton(i18n("settings.launcher.launcher_log.reveal"));
openLogFolderButton.setOnAction(e -> openLogFolder());
openLogFolderButton.getStyleClass().add("jfx-button-border");
if (LOG.getLogFile() == null)
openLogFolderButton.setDisable(true);
JFXButton logButton = FXUtils.newBorderButton(i18n("settings.launcher.launcher_log.export"));
logButton.setOnAction(e -> onExportLogs());
HBox buttonBox = new HBox();
buttonBox.setSpacing(10);
buttonBox.getChildren().addAll(openLogFolderButton, logButton);
BorderPane.setAlignment(buttonBox, Pos.CENTER_RIGHT);
debugPane.setRight(buttonBox);
settingsPane.getContent().add(debugPane);
}
rootPane.getChildren().add(settingsPane);
}
scroll.setContent(rootPane);
}
}
public void openLogFolder() {
FXUtils.openFolder(LOG.getLogFile().getParent());
}
protected abstract void onUpdate();
protected abstract void onExportLogs();
protected abstract void onSponsor();
protected abstract void clearCacheDirectory();
}
@@ -40,7 +40,6 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import org.jackhuang.hmcl.game.LauncherHelper;
@@ -168,6 +167,7 @@ public class TerracottaControllerPage extends StackPane {
progressProperty.set(0);
TextFlow body = FXUtils.segmentToTextFlow(i18n("terracotta.confirm.desc"), Controllers::onHyperlinkAction);
body.getStyleClass().add("terracotta-hint");
body.setLineSpacing(4);
LineButton download = LineButton.of();
@@ -211,6 +211,7 @@ public class TerracottaControllerPage extends StackPane {
progressProperty.set(1);
TextFlow flow = FXUtils.segmentToTextFlow(i18n("terracotta.confirm.desc"), Controllers::onHyperlinkAction);
flow.getStyleClass().add("terracotta-hint");
flow.setLineSpacing(4);
LineButton host = LineButton.of();
@@ -231,7 +232,10 @@ public class TerracottaControllerPage extends StackPane {
MessageDialogPane.MessageType.QUESTION
).addAction(i18n("version.launch"), () -> {
Profile profile = Profiles.getSelectedProfile();
Versions.launch(profile, profile.getSelectedVersion(), LauncherHelper::setKeep);
Versions.launch(profile, profile.getSelectedVersion(), launcherHelper -> {
launcherHelper.setKeep();
launcherHelper.setDisableOfflineSkin();
});
}).addCancel(i18n("terracotta.status.waiting.host.launch.skip"), () -> {
TerracottaState.HostScanning s1 = TerracottaManager.setScanning();
if (s1 != null) {
@@ -282,6 +286,7 @@ public class TerracottaControllerPage extends StackPane {
progressProperty.set(-1);
TextFlow body = FXUtils.segmentToTextFlow(i18n("terracotta.status.scanning.desc"), Controllers::onHyperlinkAction);
body.getStyleClass().add("terracotta-hint");
body.setLineSpacing(4);
LineButton room = LineButton.of();
@@ -626,9 +631,9 @@ public class TerracottaControllerPage extends StackPane {
}
HBox secondLine = new HBox();
secondLine.getStyleClass().add("second-line");
{
Text text = new Text(button.subTitle.get());
text.setFill(new Color(0, 0, 0, 0.5));
TextFlow lblSubtitle = new TextFlow(text);
lblSubtitle.getStyleClass().add("subtitle");
@@ -24,7 +24,6 @@ import javafx.geometry.Insets;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.terracotta.TerracottaMetadata;
@@ -71,7 +70,10 @@ public class TerracottaPage extends DecoratorAnimatedPage implements DecoratorPa
AdvancedListBox toolbar = new AdvancedListBox()
.addNavigationDrawerItem(i18n("version.launch"), SVG.ROCKET_LAUNCH, () -> {
Profile profile = Profiles.getSelectedProfile();
Versions.launch(profile, profile.getSelectedVersion(), LauncherHelper::setKeep);
Versions.launch(profile, profile.getSelectedVersion(), launcherHelper -> {
launcherHelper.setKeep();
launcherHelper.setDisableOfflineSkin();
});
}, item -> {
instanceChangeListenerHolder = FXUtils.onWeakChangeAndOperate(Profiles.selectedVersionProperty(),
instanceName -> item.setSubtitle(StringUtils.isNotBlank(instanceName) ? instanceName : i18n("version.empty"))
@@ -35,6 +35,7 @@ import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
@@ -60,16 +61,24 @@ import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.NotNull;
import java.lang.ref.WeakReference;
import java.net.URI;
import java.util.*;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class DownloadListPage extends Control implements DecoratorPage, VersionPage.VersionLoadable {
protected final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
@@ -523,6 +532,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
var iconCache = new WeakHashMap<String, WeakReference<CompletableFuture<Image>>>();
listView.setCellFactory(x -> new FloatListCell<>(listView) {
private final TwoLineListItem content = new TwoLineListItem();
private final ImageView imageView = new ImageView();
@@ -549,8 +559,64 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
dataItem.getCategories().stream()
.map(category -> getSkinnable().getLocalizedCategory(category))
.forEach(content::addTag);
if (StringUtils.isNotBlank(dataItem.getIconUrl())) {
imageView.imageProperty().bind(FXUtils.newRemoteImage(dataItem.getIconUrl(), 80, 80, true, true));
loadIcon(dataItem);
}
private void loadIcon(RemoteMod mod) {
if (StringUtils.isBlank(mod.getIconUrl())) {
imageView.setImage(null);
return;
}
WeakReference<CompletableFuture<Image>> cacheRef = iconCache.get(mod.getIconUrl());
CompletableFuture<Image> cache;
if (cacheRef != null && (cache = cacheRef.get()) != null) {
loadIcon(cache, mod.getIconUrl());
return;
}
URI iconUrl = NetworkUtils.toURIOrNull(mod.getIconUrl());
if (iconUrl == null) {
imageView.setImage(null);
return;
}
CompletableFuture<Image> future = new CompletableFuture<>();
WeakReference<CompletableFuture<Image>> futureRef = new WeakReference<>(future);
iconCache.put(mod.getIconUrl(), futureRef);
FXUtils.getRemoteImageTask(iconUrl, 80, 80, true, true)
.whenComplete(Schedulers.defaultScheduler(), (result, exception) -> {
if (exception == null) {
future.complete(result);
} else {
LOG.warning("Failed to load image from " + iconUrl, exception);
future.completeExceptionally(exception);
}
}).start();
loadIcon(future, mod.getIconUrl());
}
private void loadIcon(@NotNull CompletableFuture<Image> future,
@NotNull String iconUrl) {
Image image;
try {
image = future.getNow(null);
} catch (CancellationException | CompletionException ignored) {
imageView.setImage(null);
return;
}
if (image != null) {
imageView.setImage(image);
} else {
imageView.setImage(null);
future.thenAcceptAsync(result -> {
RemoteMod item = getItem();
if (item != null && iconUrl.equals(item.getIconUrl())) {
this.imageView.setImage(result);
}
}, Schedulers.javafx());
}
}
});
@@ -85,10 +85,14 @@ public class GameListItem extends Control {
Versions.openFolder(profile, version);
}
public void launch() {
public void testGame() {
Versions.testGame(profile, version);
}
public void launch() {
Versions.launch(profile, version);
}
public void modifyGameSettings() {
Versions.modifyGameSettings(profile, version);
}
@@ -44,7 +44,7 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
JFXPopup popup = new JFXPopup(menu);
menu.getContent().setAll(
new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch.test"), () -> currentSkinnable.launch(), popup),
new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch.test"), () -> currentSkinnable.testGame(), popup),
new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), () -> currentSkinnable.generateLaunchScript(), popup),
new MenuSeparator(),
new IconedMenuItem(SVG.SETTINGS, i18n("version.manage.manage"), () -> currentSkinnable.modifyGameSettings(), popup),
@@ -87,7 +87,7 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
{
JFXButton btnLaunch = new JFXButton();
btnLaunch.setOnAction(e -> skinnable.launch());
btnLaunch.setOnAction(e -> skinnable.testGame());
btnLaunch.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnLaunch, Pos.CENTER);
btnLaunch.setGraphic(FXUtils.limitingSize(SVG.ROCKET_LAUNCH.createIcon(24), 24, 24));
@@ -18,16 +18,15 @@
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXCheckBox;
import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.mod.LocalModFile;
@@ -38,6 +37,7 @@ import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.JFXCheckBoxTableCell;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
@@ -72,9 +72,10 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
getStyleClass().add("gray-background");
TableColumn<ModUpdateObject, Boolean> enabledColumn = new TableColumn<>();
CheckBox allEnabledBox = new CheckBox();
var allEnabledBox = new JFXCheckBox();
enabledColumn.setStyle("-fx-alignment: CENTER;");
enabledColumn.setGraphic(allEnabledBox);
enabledColumn.setCellFactory(CheckBoxTableCell.forTableColumn(enabledColumn));
enabledColumn.setCellFactory(JFXCheckBoxTableCell.forTableColumn(enabledColumn));
setupCellValueFactory(enabledColumn, ModUpdateObject::enabledProperty);
enabledColumn.setEditable(true);
enabledColumn.setMaxWidth(40);
@@ -101,6 +102,7 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
TableView<ModUpdateObject> table = new TableView<>(objects);
table.setEditable(true);
table.getColumns().setAll(enabledColumn, fileNameColumn, currentVersionColumn, targetVersionColumn, sourceColumn);
setMargin(table, new Insets(10, 10, 5, 10));
setCenter(table);
@@ -117,6 +119,7 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
JFXButton cancelButton = FXUtils.newRaisedButton(i18n("button.cancel"));
cancelButton.setOnAction(e -> fireEvent(new PageCloseEvent()));
onEscPressed(this, cancelButton::fire);
onEscPressed(table, cancelButton::fire);
actions.getChildren().setAll(exportListButton, nextButton, cancelButton);
setBottom(actions);
@@ -0,0 +1,282 @@
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXListView;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.resourcepack.ResourcepackFile;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.ListPageBase;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class ResourcepackListPage extends ListPageBase<ResourcepackListPage.ResourcepackInfoObject> implements VersionPage.VersionLoadable {
private Path resourcepackDirectory;
public ResourcepackListPage() {
FXUtils.applyDragListener(this, file -> file.getFileName().toString().endsWith(".zip"), this::addFiles);
}
@Override
protected Skin<?> createDefaultSkin() {
return new ResourcepackListPageSkin(this);
}
@Override
public void loadVersion(Profile profile, String version) {
this.resourcepackDirectory = profile.getRepository().getResourcepacksDirectory(version);
try {
if (!Files.exists(resourcepackDirectory)) {
Files.createDirectories(resourcepackDirectory);
}
} catch (IOException e) {
LOG.warning("Failed to create resourcepack directory" + resourcepackDirectory, e);
}
refresh();
}
public void refresh() {
if (resourcepackDirectory == null || !Files.isDirectory(resourcepackDirectory)) return;
setLoading(true);
Task.supplyAsync(Schedulers.io(), () -> {
try (Stream<Path> stream = Files.list(resourcepackDirectory)) {
return stream.sorted(Comparator.comparing(FileUtils::getName))
.flatMap(item -> {
try {
return Stream.of(ResourcepackFile.parse(item)).filter(Objects::nonNull).map(ResourcepackInfoObject::new);
} catch (IOException e) {
LOG.warning("Failed to load resourcepack " + item, e);
return Stream.empty();
}
})
.toList();
}
}).whenComplete(Schedulers.javafx(), ((result, exception) -> {
if (exception == null) {
getItems().setAll(result);
} else {
LOG.warning("Failed to load resourcepacks", exception);
getItems().clear();
}
setLoading(false);
})).start();
}
public void addFiles(List<Path> files) {
if (resourcepackDirectory == null) return;
try {
for (Path file : files) {
Path target = resourcepackDirectory.resolve(file.getFileName());
if (!Files.exists(target)) {
Files.copy(file, target);
}
}
} catch (IOException e) {
LOG.warning("Failed to add resourcepacks", e);
Controllers.dialog(i18n("resourcepack.add.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
}
refresh();
}
public void onAddFiles() {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(i18n("resourcepack.add"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("resourcepack"), "*.zip"));
List<Path> files = FileUtils.toPaths(fileChooser.showOpenMultipleDialog(Controllers.getStage()));
if (files != null && !files.isEmpty()) {
addFiles(files);
}
}
private void onDownload() {
Controllers.getDownloadPage().showResourcepackDownloads();
Controllers.navigate(Controllers.getDownloadPage());
}
private static final class ResourcepackListPageSkin extends SkinBase<ResourcepackListPage> {
private final JFXListView<ResourcepackInfoObject> listView;
private ResourcepackListPageSkin(ResourcepackListPage control) {
super(control);
StackPane pane = new StackPane();
pane.setPadding(new Insets(10));
pane.getStyleClass().addAll("notice-pane");
ComponentList root = new ComponentList();
root.getStyleClass().add("no-padding");
listView = new JFXListView<>();
HBox toolbar = new HBox();
toolbar.setAlignment(Pos.CENTER_LEFT);
toolbar.setPickOnBounds(false);
toolbar.getChildren().setAll(
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, control::refresh),
createToolbarButton2(i18n("resourcepack.add"), SVG.ADD, control::onAddFiles),
createToolbarButton2(i18n("resourcepack.download"), SVG.DOWNLOAD, control::onDownload)
);
root.getContent().add(toolbar);
SpinnerPane center = new SpinnerPane();
ComponentList.setVgrow(center, Priority.ALWAYS);
center.getStyleClass().add("large-spinner-pane");
center.loadingProperty().bind(control.loadingProperty());
Holder<Object> lastCell = new Holder<>();
listView.setCellFactory(x -> new ResourcepackListCell(listView, lastCell, control));
Bindings.bindContent(listView.getItems(), control.getItems());
center.setContent(listView);
root.getContent().add(center);
pane.getChildren().setAll(root);
getChildren().setAll(pane);
}
}
public static class ResourcepackInfoObject {
private final ResourcepackFile file;
private WeakReference<Image> iconCache;
public ResourcepackInfoObject(ResourcepackFile file) {
this.file = file;
}
public ResourcepackFile getFile() {
return file;
}
Image getIcon() {
Image image = null;
if (iconCache != null && (image = iconCache.get()) != null) {
return image;
}
byte[] iconData = file.getIcon();
if (iconData != null) {
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(iconData)) {
image = new Image(inputStream, 64, 64, true, true);
} catch (Exception e) {
LOG.warning("Failed to load resourcepack icon " + file.getPath(), e);
}
}
if (image == null || image.isError() || image.getWidth() <= 0 || image.getHeight() <= 0 ||
(Math.abs(image.getWidth() - image.getHeight()) >= 1)) {
image = FXUtils.newBuiltinImage("/assets/img/unknown_pack.png");
}
iconCache = new WeakReference<>(image);
return image;
}
}
private static final class ResourcepackListCell extends MDListCell<ResourcepackInfoObject> {
private final ImageView imageView = new ImageView();
private final TwoLineListItem content = new TwoLineListItem();
private final JFXButton btnReveal = new JFXButton();
private final JFXButton btnDelete = new JFXButton();
private final ResourcepackListPage page;
public ResourcepackListCell(JFXListView<ResourcepackInfoObject> listView, Holder<Object> lastCell, ResourcepackListPage page) {
super(listView, lastCell);
this.page = page;
BorderPane root = new BorderPane();
root.getStyleClass().add("md-list-cell");
root.setPadding(new Insets(8));
HBox left = new HBox(8);
left.setAlignment(Pos.CENTER);
FXUtils.limitSize(imageView, 32, 32);
left.getChildren().add(imageView);
left.setPadding(new Insets(0, 8, 0, 0));
FXUtils.setLimitWidth(left, 48);
root.setLeft(left);
HBox.setHgrow(content, Priority.ALWAYS);
root.setCenter(content);
btnReveal.getStyleClass().add("toggle-icon4");
btnReveal.setGraphic(SVG.FOLDER_OPEN.createIcon());
btnDelete.getStyleClass().add("toggle-icon4");
btnDelete.setGraphic(SVG.DELETE_FOREVER.createIcon());
HBox right = new HBox(8);
right.setAlignment(Pos.CENTER_RIGHT);
right.getChildren().setAll(btnReveal, btnDelete);
root.setRight(right);
getContainer().getChildren().add(new RipplerContainer(root));
}
@Override
protected void updateControl(ResourcepackListPage.ResourcepackInfoObject item, boolean empty) {
if (empty || item == null) {
return;
}
ResourcepackFile file = item.getFile();
imageView.setImage(item.getIcon());
content.setTitle(file.getName());
LocalModFile.Description description = file.getDescription();
content.setSubtitle(description != null ? description.toString() : "");
FXUtils.installFastTooltip(btnReveal, i18n("reveal.in_file_manager"));
btnReveal.setOnAction(event -> FXUtils.showFileInExplorer(file.getPath()));
btnDelete.setOnAction(event ->
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"),
() -> onDelete(file), null));
}
private void onDelete(ResourcepackFile file) {
try {
if (Files.isDirectory(file.getPath())) {
FileUtils.deleteDirectory(file.getPath());
} else {
Files.delete(file.getPath());
}
page.refresh();
} catch (IOException e) {
Controllers.dialog(i18n("resourcepack.delete.failed", e.getMessage()), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
LOG.warning("Failed to delete resourcepack", e);
}
}
}
}
@@ -56,6 +56,7 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
private final TabHeader.Tab<ModListPage> modListTab = new TabHeader.Tab<>("modListTab");
private final TabHeader.Tab<WorldListPage> worldListTab = new TabHeader.Tab<>("worldList");
private final TabHeader.Tab<SchematicsPage> schematicsTab = new TabHeader.Tab<>("schematicsTab");
private final TabHeader.Tab<ResourcepackListPage> resourcePackTab = new TabHeader.Tab<>("resourcePackTab");
private final TransitionPane transitionPane = new TransitionPane();
private final BooleanProperty currentVersionUpgradable = new SimpleBooleanProperty();
private final ObjectProperty<Profile.ProfileVersion> version = new SimpleObjectProperty<>();
@@ -67,10 +68,11 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
versionSettingsTab.setNodeSupplier(loadVersionFor(() -> new VersionSettingsPage(false)));
installerListTab.setNodeSupplier(loadVersionFor(InstallerListPage::new));
modListTab.setNodeSupplier(loadVersionFor(ModListPage::new));
resourcePackTab.setNodeSupplier(loadVersionFor(ResourcepackListPage::new));
worldListTab.setNodeSupplier(loadVersionFor(WorldListPage::new));
schematicsTab.setNodeSupplier(loadVersionFor(SchematicsPage::new));
tab = new TabHeader(transitionPane, versionSettingsTab, installerListTab, modListTab, worldListTab, schematicsTab);
tab = new TabHeader(transitionPane, versionSettingsTab, installerListTab, modListTab, resourcePackTab, worldListTab, schematicsTab);
tab.select(versionSettingsTab);
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
@@ -130,6 +132,8 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
installerListTab.getNode().loadVersion(profile, version);
if (modListTab.isInitialized())
modListTab.getNode().loadVersion(profile, version);
if (resourcePackTab.isInitialized())
resourcePackTab.getNode().loadVersion(profile, version);
if (worldListTab.isInitialized())
worldListTab.getNode().loadVersion(profile, version);
if (schematicsTab.isInitialized())
@@ -238,6 +242,7 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
.addNavigationDrawerTab(control.tab, control.versionSettingsTab, i18n("settings.game"), SVG.SETTINGS, SVG.SETTINGS_FILL)
.addNavigationDrawerTab(control.tab, control.installerListTab, i18n("settings.tabs.installers"), SVG.DEPLOYED_CODE, SVG.DEPLOYED_CODE_FILL)
.addNavigationDrawerTab(control.tab, control.modListTab, i18n("mods.manage"), SVG.EXTENSION, SVG.EXTENSION_FILL)
.addNavigationDrawerTab(control.tab, control.resourcePackTab, i18n("resourcepack.manage"), SVG.TEXTURE)
.addNavigationDrawerTab(control.tab, control.worldListTab, i18n("world.manage"), SVG.PUBLIC)
.addNavigationDrawerTab(control.tab, control.schematicsTab, i18n("schematics.manage"), SVG.SCHEMA, SVG.SCHEMA_FILL);
VBox.setVgrow(sideBar, Priority.ALWAYS);
@@ -111,6 +111,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
private final MultiFileItem.FileOption<GameDirectoryType> gameDirCustomOption;
private final JFXComboBox<ProcessPriority> cboProcessPriority;
private final OptionToggleButton showLogsPane;
private final OptionToggleButton enableDebugLogOutputPane;
private final ImagePickerItem iconPickerItem;
private final ChangeListener<Collection<JavaRuntime>> javaListChangeListener;
@@ -410,6 +411,9 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
showLogsPane = new OptionToggleButton();
showLogsPane.setTitle(i18n("settings.show_log"));
enableDebugLogOutputPane = new OptionToggleButton();
enableDebugLogOutputPane.setTitle(i18n("settings.enable_debug_log_output"));
BorderPane processPriorityPane = new BorderPane();
{
Label label = new Label(i18n("settings.advanced.process_priority"));
@@ -474,6 +478,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
launcherVisibilityPane,
dimensionPane,
showLogsPane,
enableDebugLogOutputPane,
processPriorityPane,
serverPane,
showAdvancedSettingPane
@@ -551,6 +556,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
chkAutoAllocate.selectedProperty().unbindBidirectional(lastVersionSetting.autoMemoryProperty());
chkFullscreen.selectedProperty().unbindBidirectional(lastVersionSetting.fullscreenProperty());
showLogsPane.selectedProperty().unbindBidirectional(lastVersionSetting.showLogsProperty());
enableDebugLogOutputPane.selectedProperty().unbindBidirectional(lastVersionSetting.enableDebugLogOutputProperty());
FXUtils.unbindEnum(cboLauncherVisibility, lastVersionSetting.launcherVisibilityProperty());
FXUtils.unbindEnum(cboProcessPriority, lastVersionSetting.processPriorityProperty());
@@ -585,6 +591,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
chkAutoAllocate.selectedProperty().bindBidirectional(versionSetting.autoMemoryProperty());
chkFullscreen.selectedProperty().bindBidirectional(versionSetting.fullscreenProperty());
showLogsPane.selectedProperty().bindBidirectional(versionSetting.showLogsProperty());
enableDebugLogOutputPane.selectedProperty().bindBidirectional(versionSetting.enableDebugLogOutputProperty());
FXUtils.bindEnum(cboLauncherVisibility, versionSetting.launcherVisibilityProperty());
FXUtils.bindEnum(cboProcessPriority, versionSetting.processPriorityProperty());
@@ -23,10 +23,7 @@ import javafx.stage.FileChooser;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.game.GameDirectoryType;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.*;
@@ -200,7 +197,8 @@ public final class Versions {
}
}
public static void generateLaunchScript(Profile profile, String id) {
@SafeVarargs
public static void generateLaunchScript(Profile profile, String id, Consumer<LauncherHelper>... injecters) {
if (!checkVersionForLaunching(profile, id))
return;
ensureSelectedAccount(account -> {
@@ -209,23 +207,35 @@ public final class Versions {
if (Files.isDirectory(repository.getRunDirectory(id)))
chooser.setInitialDirectory(repository.getRunDirectory(id).toFile());
chooser.setTitle(i18n("version.launch_script.save"));
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
chooser.getExtensionFilters().add(
new FileChooser.ExtensionFilter(i18n("extension.command"), "*.command")
);
}
chooser.getExtensionFilters().add(OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
? new FileChooser.ExtensionFilter(i18n("extension.bat"), "*.bat")
: new FileChooser.ExtensionFilter(i18n("extension.sh"), "*.sh"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.ps1"), "*.ps1"));
Path file = FileUtils.toPath(chooser.showSaveDialog(Controllers.getStage()));
if (file != null)
new LauncherHelper(profile, account, id).makeLaunchScript(file);
if (file != null) {
LauncherHelper launcherHelper = new LauncherHelper(profile, account, id);
for (Consumer<LauncherHelper> injecter : injecters) {
injecter.accept(launcherHelper);
}
launcherHelper.makeLaunchScript(file);
}
});
}
public static void launch(Profile profile, String id, Consumer<LauncherHelper> injecter) {
@SafeVarargs
public static void launch(Profile profile, String id, Consumer<LauncherHelper>... injecters) {
if (!checkVersionForLaunching(profile, id))
return;
ensureSelectedAccount(account -> {
LauncherHelper launcherHelper = new LauncherHelper(profile, account, id);
if (injecter != null)
for (Consumer<LauncherHelper> injecter : injecters) {
injecter.accept(launcherHelper);
}
launcherHelper.launch();
});
}
@@ -234,6 +244,16 @@ public final class Versions {
launch(profile, id, LauncherHelper::setTestMode);
}
public static void launchAndEnterWorld(Profile profile, String id, String worldFolderName) {
launch(profile, id, launcherHelper ->
launcherHelper.setQuickPlayOption(new QuickPlayOption.SinglePlayer(worldFolderName)));
}
public static void generateLaunchScriptForQuickEnterWorld(Profile profile, String id, String worldFolderName) {
generateLaunchScript(profile, id, launcherHelper ->
launcherHelper.setQuickPlayOption(new QuickPlayOption.SinglePlayer(worldFolderName)));
}
private static boolean checkVersionForLaunching(Profile profile, String id) {
if (id == null || !profile.getRepository().isLoaded() || !profile.getRepository().hasVersion(id)) {
JFXButton gotoDownload = new JFXButton(i18n("version.empty.launch.goto_download"));
@@ -39,6 +39,7 @@ import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jetbrains.annotations.NotNull;
import java.nio.file.*;
@@ -245,7 +246,7 @@ public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.Backup
item.setSubtitle(formatDateTime(skinnable.getBackupTime()) + (skinnable.count == 0 ? "" : " (" + skinnable.count + ")"));
if (world.getGameVersion() != null)
item.addTag(world.getGameVersion());
item.addTag(I18n.getDisplayVersion(world.getGameVersion()));
}
{
@@ -23,6 +23,7 @@ import javafx.scene.control.Skin;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.wizard.WizardSinglePage;
import org.jackhuang.hmcl.util.i18n.I18n;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -41,7 +42,8 @@ public class WorldExportPage extends WizardSinglePage {
this.world = world;
path.set(export.toString());
gameVersion.set(world.getGameVersion());
if (world.getGameVersion() != null)
gameVersion.set(I18n.getDisplayVersion(world.getGameVersion()));
worldName.set(world.getWorldName());
}
@@ -122,7 +122,8 @@ public final class WorldInfoPage extends SpinnerPane {
Label gameVersionLabel = new Label();
FXUtils.copyOnDoubleClick(gameVersionLabel);
gameVersionLabel.setText(world.getGameVersion());
if (world.getGameVersion() != null)
gameVersionLabel.setText(world.getGameVersion().toNormalizedString());
BorderPane.setAlignment(gameVersionLabel, Pos.CENTER_RIGHT);
gameVersionPane.setRight(gameVersionLabel);
}
@@ -22,6 +22,7 @@ import javafx.scene.control.Skin;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.game.WorldLockedException;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
@@ -39,11 +40,15 @@ public final class WorldListItem extends Control {
private final World world;
private final Path backupsDir;
private final WorldListPage parent;
private final Profile profile;
private final String id;
public WorldListItem(WorldListPage parent, World world, Path backupsDir) {
public WorldListItem(WorldListPage parent, World world, Path backupsDir, Profile profile, String id) {
this.world = world;
this.backupsDir = backupsDir;
this.parent = parent;
this.profile = profile;
this.id = id;
}
@Override
@@ -91,6 +96,14 @@ public final class WorldListItem extends Control {
}
public void showManagePage() {
Controllers.navigate(new WorldManagePage(world, backupsDir));
Controllers.navigate(new WorldManagePage(world, backupsDir, profile, id));
}
public void launch() {
Versions.launchAndEnterWorld(profile, id, world.getFileName());
}
public void generateLaunchScript() {
Versions.generateLaunchScriptForQuickEnterWorld(profile, id, world.getFileName());
}
}
@@ -32,7 +32,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.ChunkBaseApp;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jackhuang.hmcl.util.i18n.I18n;
import java.time.Instant;
@@ -72,10 +72,10 @@ public final class WorldListItemSkin extends SkinBase<WorldListItem> {
item.setMouseTransparent(true);
if (world.getWorldName() != null)
item.setTitle(parseColorEscapes(world.getWorldName()));
item.setSubtitle(i18n("world.datetime", formatDateTime(Instant.ofEpochMilli(world.getLastPlayed())), world.getGameVersion() == null ? i18n("message.unknown") : world.getGameVersion()));
item.setSubtitle(i18n("world.datetime", formatDateTime(Instant.ofEpochMilli(world.getLastPlayed()))));
if (world.getGameVersion() != null)
item.addTag(world.getGameVersion());
item.addTag(I18n.getDisplayVersion(world.getGameVersion()));
if (world.isLocked())
item.addTag(i18n("world.locked"));
}
@@ -115,8 +115,19 @@ public final class WorldListItemSkin extends SkinBase<WorldListItem> {
WorldListItem item = getSkinnable();
World world = item.getWorld();
popupMenu.getContent().addAll(
new IconedMenuItem(SVG.SETTINGS, i18n("world.manage.button"), item::showManagePage, popup));
if (world.getGameVersion() != null && world.getGameVersion().isAtLeast("1.20", "23w14a")) {
IconedMenuItem launchItem = new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch_and_enter_world"), item::launch, popup);
launchItem.setDisable(world.isLocked());
popupMenu.getContent().add(launchItem);
popupMenu.getContent().addAll(
new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), item::generateLaunchScript, popup),
new MenuSeparator()
);
}
popupMenu.getContent().add(new IconedMenuItem(SVG.SETTINGS, i18n("world.manage.button"), item::showManagePage, popup));
if (ChunkBaseApp.isSupported(world)) {
popupMenu.getContent().addAll(
@@ -126,7 +137,7 @@ public final class WorldListItemSkin extends SkinBase<WorldListItem> {
new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(world), popup)
);
if (GameVersionNumber.compare(world.getGameVersion(), "1.13") >= 0) {
if (world.getGameVersion() != null && world.getGameVersion().compareTo("1.13") >= 0) {
popupMenu.getContent().add(new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"),
() -> ChunkBaseApp.openEndCityFinder(world), popup));
}
@@ -28,6 +28,7 @@ import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
@@ -49,7 +50,7 @@ public final class WorldListPage extends ListPageBase<WorldListItem> implements
private List<World> worlds;
private Profile profile;
private String id;
private String gameVersion;
private GameVersionNumber gameVersion;
public WorldListPage() {
FXUtils.applyDragListener(this, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> {
@@ -60,7 +61,7 @@ public final class WorldListPage extends ListPageBase<WorldListItem> implements
if (worlds != null)
itemsProperty().setAll(worlds.stream()
.filter(world -> isShowAll() || world.getGameVersion() == null || world.getGameVersion().equals(gameVersion))
.map(world -> new WorldListItem(this, world, backupsDir)).toList());
.map(world -> new WorldListItem(this, world, backupsDir, profile, id)).toList());
});
}
@@ -87,7 +88,7 @@ public final class WorldListPage extends ListPageBase<WorldListItem> implements
return;
setLoading(true);
Task.runAsync(() -> gameVersion = profile.getRepository().getGameVersion(id).orElse(null))
Task.runAsync(() -> gameVersion = profile.getRepository().getGameVersion(id).map(GameVersionNumber::asGameVersion).orElse(null))
.thenApplyAsync(unused -> {
try (Stream<World> stream = World.getWorlds(savesDir)) {
return stream.parallel().collect(Collectors.toList());
@@ -99,7 +100,7 @@ public final class WorldListPage extends ListPageBase<WorldListItem> implements
if (exception == null) {
itemsProperty().setAll(result.stream()
.filter(world -> isShowAll() || world.getGameVersion() == null || world.getGameVersion().equals(gameVersion))
.map(world -> new WorldListItem(this, world, backupsDir))
.map(world -> new WorldListItem(this, world, backupsDir, profile, id))
.collect(Collectors.toList()));
} else {
LOG.warning("Failed to load world list page", exception);
@@ -130,7 +131,7 @@ public final class WorldListPage extends ListPageBase<WorldListItem> implements
Controllers.prompt(i18n("world.name.enter"), (name, resolve, reject) -> {
Task.runAsync(() -> world.install(savesDir, name))
.whenComplete(Schedulers.javafx(), () -> {
itemsProperty().add(new WorldListItem(this, new World(savesDir.resolve(name)), backupsDir));
itemsProperty().add(new WorldListItem(this, new World(savesDir.resolve(name)), backupsDir, profile, id));
resolve.run();
}, e -> {
if (e instanceof FileAlreadyExistsException)
@@ -26,6 +26,7 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
@@ -33,7 +34,6 @@ import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.ChunkBaseApp;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.io.IOException;
import java.nio.channels.FileChannel;
@@ -50,6 +50,8 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
private final ObjectProperty<State> state;
private final World world;
private final Path backupsDir;
private final Profile profile;
private final String id;
private final TabHeader header;
private final TabHeader.Tab<WorldInfoPage> worldInfoTab = new TabHeader.Tab<>("worldInfoPage");
@@ -60,10 +62,13 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
private FileChannel sessionLockChannel;
public WorldManagePage(World world, Path backupsDir) {
public WorldManagePage(World world, Path backupsDir, Profile profile, String id) {
this.world = world;
this.backupsDir = backupsDir;
this.profile = profile;
this.id = id;
this.worldInfoTab.setNodeSupplier(() -> new WorldInfoPage(this));
this.worldBackupsTab.setNodeSupplier(() -> new WorldBackupsPage(this));
this.datapackTab.setNodeSupplier(() -> new DatapackListPage(this));
@@ -72,6 +77,13 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
this.header = new TabHeader(transitionPane, worldInfoTab, worldBackupsTab);
header.select(worldInfoTab);
// Does it need to be done in the background?
try {
sessionLockChannel = world.lock();
LOG.info("Acquired lock on world " + world.getFileName());
} catch (IOException ignored) {
}
setCenter(transitionPane);
BorderPane left = new BorderPane();
@@ -84,7 +96,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
.addNavigationDrawerTab(header, worldBackupsTab, i18n("world.backup"), SVG.ARCHIVE, SVG.ARCHIVE_FILL);
if (world.getGameVersion() != null && // old game will not write game version to level.dat
GameVersionNumber.asGameVersion(world.getGameVersion()).isAtLeast("1.13", "17w43a")) {
world.getGameVersion().isAtLeast("1.13", "17w43a")) {
header.getTabs().add(datapackTab);
sideBar.addNavigationDrawerTab(header, datapackTab, i18n("world.datapack"), SVG.EXTENSION, SVG.EXTENSION_FILL);
}
@@ -92,6 +104,12 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
left.setTop(sideBar);
AdvancedListBox toolbar = new AdvancedListBox();
if (world.getGameVersion() != null && world.getGameVersion().isAtLeast("1.20", "23w14a")) {
toolbar.addNavigationDrawerItem(i18n("version.launch"), SVG.ROCKET_LAUNCH, this::launch, null);
toolbar.addNavigationDrawerItem(i18n("version.launch_script"), SVG.SCRIPT, this::generateLaunchScript, null);
}
if (ChunkBaseApp.isSupported(world)) {
PopupMenu popupMenu = new PopupMenu();
JFXPopup popup = new JFXPopup(popupMenu);
@@ -102,7 +120,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(world), popup)
);
if (GameVersionNumber.compare(world.getGameVersion(), "1.13") >= 0) {
if (world.getGameVersion() != null && world.getGameVersion().compareTo("1.13") >= 0) {
popupMenu.getContent().add(
new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), () -> ChunkBaseApp.openEndCityFinder(world), popup));
}
@@ -118,12 +136,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0));
left.setBottom(toolbar);
// Does it need to be done in the background?
try {
sessionLockChannel = world.lock();
LOG.info("Acquired lock on world " + world.getFileName());
} catch (IOException ignored) {
}
this.addEventHandler(Navigator.NavigationEvent.EXITED, this::onExited);
}
@Override
@@ -143,14 +156,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
return sessionLockChannel == null;
}
@Override
public boolean back() {
closePage();
return true;
}
@Override
public void closePage() {
public void onExited(Navigator.NavigationEvent event) {
if (sessionLockChannel != null) {
try {
sessionLockChannel.close();
@@ -162,4 +168,13 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
sessionLockChannel = null;
}
}
public void launch() {
fireEvent(new PageCloseEvent());
Versions.launchAndEnterWorld(profile, id, world.getFileName());
}
public void generateLaunchScript() {
Versions.generateLaunchScriptForQuickEnterWorld(profile, id, world.getFileName());
}
}
@@ -49,7 +49,7 @@ public final class ChunkBaseApp {
public static boolean isSupported(@NotNull World world) {
return world.getSeed() != null && world.getGameVersion() != null &&
GameVersionNumber.asGameVersion(world.getGameVersion()).compareTo(MIN_GAME_VERSION) >= 0;
world.getGameVersion().compareTo(MIN_GAME_VERSION) >= 0;
}
public static ChunkBaseApp newBuilder(String app, long seed) {
@@ -60,7 +60,7 @@ public final class ChunkBaseApp {
assert isSupported(world);
newBuilder("seed-map", Objects.requireNonNull(world.getSeed()))
.addPlatform(GameVersionNumber.asGameVersion(world.getGameVersion()), world.isLargeBiomes(), SEED_MAP_GAME_VERSIONS)
.addPlatform(world.getGameVersion(), world.isLargeBiomes(), SEED_MAP_GAME_VERSIONS)
.open();
}
@@ -68,7 +68,7 @@ public final class ChunkBaseApp {
assert isSupported(world);
newBuilder("stronghold-finder", Objects.requireNonNull(world.getSeed()))
.addPlatform(GameVersionNumber.asGameVersion(world.getGameVersion()), world.isLargeBiomes(), STRONGHOLD_FINDER_GAME_VERSIONS)
.addPlatform(world.getGameVersion(), world.isLargeBiomes(), STRONGHOLD_FINDER_GAME_VERSIONS)
.open();
}
@@ -76,7 +76,7 @@ public final class ChunkBaseApp {
assert isSupported(world);
newBuilder("nether-fortress-finder", Objects.requireNonNull(world.getSeed()))
.addPlatform(GameVersionNumber.asGameVersion(world.getGameVersion()), false, NETHER_FORTRESS_GAME_VERSIONS)
.addPlatform(world.getGameVersion(), false, NETHER_FORTRESS_GAME_VERSIONS)
.open();
}
@@ -84,7 +84,7 @@ public final class ChunkBaseApp {
assert isSupported(world);
newBuilder("endcity-finder", Objects.requireNonNull(world.getSeed()))
.addPlatform(GameVersionNumber.asGameVersion(world.getGameVersion()), false, END_CITY_GAME_VERSIONS)
.addPlatform(world.getGameVersion(), false, END_CITY_GAME_VERSIONS)
.open();
}
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.util.i18n;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
import org.jackhuang.hmcl.util.i18n.translator.Translator;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.PropertyKey;
@@ -76,7 +77,11 @@ public final class I18n {
return getTranslator().formatSpeed(bytes);
}
public static String getDisplaySelfVersion(RemoteVersion version) {
public static String getDisplayVersion(RemoteVersion version) {
return getTranslator().getDisplayVersion(version);
}
public static String getDisplayVersion(GameVersionNumber version) {
return getTranslator().getDisplayVersion(version);
}
@@ -72,7 +72,7 @@ public final class MinecraftWiki {
else if (wikiVersion.startsWith("1.0.0-rc2"))
wikiVersion = "1.0.0-rc2";
}
} else if (gameVersion instanceof GameVersionNumber.Snapshot) {
} else if (gameVersion instanceof GameVersionNumber.LegacySnapshot) {
return locale.i18n("wiki.version.game.snapshot", wikiVersion) + variantSuffix;
} else {
if (wikiVersion.length() >= 6 && wikiVersion.charAt(2) == 'w') {
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.util.i18n.translator;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
@@ -47,6 +48,10 @@ public class Translator {
return remoteVersion.getSelfVersion();
}
public String getDisplayVersion(GameVersionNumber versionNumber) {
return versionNumber.toNormalizedString();
}
/// @see [#formatDateTime(TemporalAccessor)]
protected DateTimeFormatter dateTimeFormatter;
@@ -92,22 +92,38 @@ public class Translator_lzh extends Translator {
appendDigitByDigit(builder, String.valueOf(release.getPatch()));
}
//noinspection StatementWithEmptyBody
if (release.getEaType() == GameVersionNumber.Release.TYPE_GA) {
// do nothing
} else if (release.getEaType() == GameVersionNumber.Release.TYPE_PRE) {
builder.append("之預");
appendDigitByDigit(builder, release.getEaVersion().toString());
} else if (release.getEaType() == GameVersionNumber.Release.TYPE_RC) {
builder.append("之候");
appendDigitByDigit(builder, release.getEaVersion().toString());
} else {
// Unsupported
return gameVersion.toString();
switch (release.getEaType()) {
case GA -> {
// do nothing
}
case PRE_RELEASE -> {
builder.append("之預");
appendDigitByDigit(builder, release.getEaVersion().toString());
}
case RELEASE_CANDIDATE -> {
builder.append("之候");
appendDigitByDigit(builder, release.getEaVersion().toString());
}
default -> {
// Unsupported
return gameVersion.toString();
}
}
switch (release.getAdditional()) {
case NONE -> {
}
case UNOBFUSCATED -> {
builder.append("涇渭");
}
default -> {
// Unsupported
return gameVersion.toString();
}
}
return builder.toString();
} else if (gameVersion instanceof GameVersionNumber.Snapshot snapshot) {
} else if (gameVersion instanceof GameVersionNumber.LegacySnapshot snapshot) {
StringBuilder builder = new StringBuilder();
appendDigitByDigit(builder, String.valueOf(snapshot.getYear()));
@@ -120,6 +136,9 @@ public class Translator_lzh extends Translator {
else
builder.append(suffix);
if (snapshot.isUnobfuscated())
builder.append("涇渭");
return builder.toString();
} else if (gameVersion instanceof GameVersionNumber.Special) {
String version = gameVersion.toString();
@@ -130,6 +149,7 @@ public class Translator_lzh extends Translator {
case "2.0_purple" -> "二點〇紫";
case "1.rv-pre1" -> "一點真視之預一";
case "3d shareware v1.34" -> "躍然享件一點三四";
case "13w12~" -> "一三週一二閏";
case "20w14infinite", "20w14~", "20w14∞" -> "二〇週一四宇";
case "22w13oneblockatatime" -> "二二週一三典";
case "23w13a_or_b" -> "二三週一三暨";
@@ -179,6 +199,11 @@ public class Translator_lzh extends Translator {
return translateGenericVersion(remoteVersion.getSelfVersion());
}
@Override
public String getDisplayVersion(GameVersionNumber versionNumber) {
return translateGameVersion(versionNumber);
}
@Override
public String formatDateTime(TemporalAccessor time) {
LocalDateTime localDateTime;
+1 -1
View File
@@ -56,5 +56,5 @@
-monet-on-surface-variant-transparent-38: #45465161;
-monet-surface-container-low-transparent-80: #F5F2FACC;
-monet-secondary-container-transparent-80: #D0D5FDCC;
-warning-tag-background: #F1AEB5;
-monet-inverse-surface-transparent-80: #303036CC;
}
@@ -0,0 +1,20 @@
.hint {
-fx-hyperlink-fill: -monet-inverse-primary;
}
.two-line-list-item {
-fixed-warning-tag-background: #2c0b0e;
}
.log-window {
-fixed-log-toggle-selected: #DEE2E6;
-fixed-log-toggle-unselected: #6C757D;
-fixed-log-text-fill: #FFFFFF;
-fixed-log-trace: #495057;
-fixed-log-debug: #343A40;
-fixed-log-info: #212529;
-fixed-log-warn: #331904;
-fixed-log-error: #58151C;
-fixed-log-fatal: #842029;
-fixed-log-selected: #6C757D;
}
@@ -0,0 +1,20 @@
.hint {
-fx-hyperlink-fill: -monet-primary;
}
.two-line-list-item {
-fixed-warning-tag-background: #F1AEB5;
}
.log-window {
-fixed-log-toggle-selected: #000000;
-fixed-log-toggle-unselected: #6C757D;
-fixed-log-text-fill: #000000;
-fixed-log-trace: #EEE9E0;
-fixed-log-debug: #EEE9E0;
-fixed-log-info: #FFFFFF;
-fixed-log-warn: #FFEECC;
-fixed-log-error: #FFCCBB;
-fixed-log-fatal: #F7A699;
-fixed-log-selected: #C4C4C4;
}
+213 -62
View File
@@ -41,6 +41,14 @@
-fx-text-fill: -monet-on-surface-variant;
}
.hyperlink {
-fx-text-fill: -monet-primary;
}
.hyperlink:focused {
-fx-border-color: -monet-primary;
}
.hint {
-fx-background-radius: 5;
-fx-border-width: 1;
@@ -48,6 +56,11 @@
-fx-padding: 6;
}
.hint .hyperlink {
-fx-fill: -monet-primary;
-fx-text-fill: -monet-primary;
}
.hint.info {
-fx-background-color: -monet-primary-fixed-dim;
-fx-border-color: -monet-outline;
@@ -76,12 +89,17 @@
}
.hint.warning {
-fx-background-color: -monet-tertiary-container;
-fx-background-color: -monet-tertiary-fixed-dim;
-fx-border-color: -monet-outline;
}
.hint.warning Text, .hint.warning .svg {
-fx-fill: -monet-on-tertiary-container;
-fx-fill: -monet-on-tertiary-fixed;
}
.hint.warning .hyperlink {
-fx-fill: -fx-hyperlink-fill;
-fx-text-fill: -fx-hyperlink-fill;
}
.skin-pane .jfx-text-field {
@@ -145,12 +163,16 @@
}
.announcement .hyperlink {
-fx-fill: #0070E0;
-fx-fill: -monet-primary;
-fx-font-size: 13px;
}
.rippler-container {
-jfx-rippler-fill: #a2a2a2;
-jfx-rippler-fill: -monet-on-surface-variant;
}
.advanced-list-item .rippler-container {
-jfx-rippler-fill: -monet-on-secondary-container;
}
.advanced-list-item > .rippler-container > .container {
@@ -243,6 +265,10 @@
-fx-spacing: 0;
}
.no-padding .advanced-list-box-content {
-fx-padding: 0 0 0 0;
}
.iconed-item {
-jfx-rippler-fill: -monet-secondary-container;
}
@@ -289,6 +315,10 @@
-fx-font-size: 12px;
}
.two-line-list-item > .second-line > .subtitle Text {
-fx-fill: -monet-on-surface-variant;
}
.two-line-list-item > .first-line > .tag {
-fx-text-fill: -monet-on-secondary-container;
-fx-background-color: -monet-secondary-container;
@@ -299,7 +329,7 @@
.two-line-list-item > .first-line > .tag-warning {
-fx-text-fill: -monet-on-error-container;
-fx-background-color: -warning-tag-background;
-fx-background-color: -fixed-warning-tag-background;
-fx-padding: 2;
-fx-font-weight: normal;
-fx-font-size: 12px;
@@ -320,6 +350,10 @@
-fx-font-size: 15px;
}
.terracotta-hint Text {
-fx-fill: -monet-on-surface;
}
.game-crash-window {
-fx-background-color: -monet-surface-container;
}
@@ -329,7 +363,7 @@
}
.game-crash-window .crash-reason-text-flow .hyperlink {
-fx-fill: #0070E0;
-fx-fill: -monet-primary;
}
.wrap-text > HBox > .subtitle {
@@ -337,14 +371,17 @@
}
.bubble {
-fx-background-color: rgba(0, 0, 0, 0.5);
-fx-background-color: -monet-inverse-surface-transparent-80;
-fx-background-radius: 2px;
-fx-text-fill: white;
}
.bubble > HBox > .two-line-list-item > .first-line > .title,
.bubble > HBox > .two-line-list-item > HBox > .subtitle {
-fx-text-fill: white;
-fx-text-fill: -monet-inverse-on-surface;
}
.bubble .svg {
-fx-fill: -monet-inverse-on-surface;
}
.sponsor-pane {
@@ -408,17 +445,6 @@
-fx-fill: -monet-primary-container;
}
/*******************************************************************************
* *
* Tooltip *
* *
******************************************************************************/
.tooltip {
-fx-text-fill: -monet-inverse-on-surface;
-fx-background-color: -monet-inverse-surface;
}
/*******************************************************************************
* *
* JFX Tab Pane *
@@ -653,11 +679,6 @@
-fx-padding: 10.0;
}
.jfx-tool-bar-button .jfx-rippler {
-jfx-rippler-fill: -monet-on-surface;
-jfx-mask-type: CIRCLE;
}
.jfx-decorator-button {
-fx-max-width: 40px;
-fx-background-radius: 5px;
@@ -742,7 +763,7 @@
}
.jfx-button .jfx-rippler {
-jfx-rippler-fill: -monet-on-primary;
-jfx-rippler-fill: -monet-on-surface;
-jfx-mask-type: CIRCLE;
-fx-padding: 0.0;
}
@@ -752,6 +773,7 @@
}
.jfx-button-raised .jfx-rippler {
-jfx-rippler-fill: -monet-on-primary;
-jfx-mask-type: CIRCLE;
-fx-padding: 0.0;
}
@@ -795,6 +817,10 @@
-jfx-unchecked-color: transparent;
}
.table-view .jfx-check-box .jfx-rippler {
-jfx-rippler-disabled: true;
}
/*******************************************************************************
* *
* JFX Progress Bar *
@@ -826,6 +852,7 @@
.jfx-text-field, .jfx-password-field, .jfx-text-area {
-fx-background-color: -monet-surface-container-highest;
-fx-highlight-fill: -monet-primary;
-fx-text-fill: -monet-on-surface;
-fx-font-weight: BOLD;
-fx-prompt-text-fill: -monet-on-surface-variant;
@@ -982,6 +1009,10 @@
-fx-text-fill: -monet-on-surface;
}
.options-list-item .label.subtitle {
-fx-text-fill: -monet-on-surface-variant;
}
.options-list-item.no-padding {
-fx-padding: 0;
}
@@ -1194,28 +1225,62 @@
* *
*******************************************************************************/
.log-toggle:selected {
-fx-background-color: transparent;
.log-toggle {
-fx-border: 1px;
-fx-border-color: black;
-fx-text-fill: black;
-fx-border-color: -fixed-log-toggle-unselected;
-fx-text-fill: -fixed-log-toggle-unselected;
}
.log-toggle {
-fx-background-color: transparent;
.log-toggle:selected {
-fx-border: 1px;
-fx-border-color: gray;
-fx-text-fill: gray;
-fx-border-color: -fixed-log-toggle-selected;
-fx-text-fill: -fixed-log-toggle-selected;
}
.log-toggle.fatal {
-fx-background-color: -fixed-log-fatal;
}
.log-toggle.error {
-fx-background-color: -fixed-log-error;
}
.log-toggle.warn {
-fx-background-color: -fixed-log-warn;
}
.log-toggle.info {
-fx-background-color: -fixed-log-info;
}
.log-toggle.debug {
-fx-background-color: -fixed-log-debug;
}
.log-toggle.trace {
-fx-background-color: -fixed-log-trace;
}
.log-window-list-cell:selected {
-fx-background-color: -fixed-log-selected;
}
.log-window {
-fx-background-color: -monet-surface-container;
}
.log-window .scroll-bar .thumb {
-fx-fill: rgba(0, 0, 0, 0.5);
-fx-fill: -monet-surface-tint;
}
.log-window .jfx-button {
-fx-text-fill: -monet-on-surface;
}
.log-window-list-cell {
-fx-text-fill: black;
-fx-text-fill: -fixed-log-text-fill;
-fx-border-width: 0 0 1 0;
-fx-border-color: #dddddd;
-fx-border-color: -monet-outline-variant;
}
.log-window-list-cell:empty {
@@ -1223,31 +1288,38 @@
}
.log-window-list-cell:fatal {
-fx-background-color: #F7A699;
-fx-text-fill: -fixed-log-text-fill;
-fx-background-color: -fixed-log-fatal;
}
.log-window-list-cell:error {
-fx-background-color: #FFCCBB;
-fx-text-fill: -fixed-log-text-fill;
-fx-background-color: -fixed-log-error;
}
.log-window-list-cell:warn {
-fx-background-color: #FFEECC;
-fx-text-fill: -fixed-log-text-fill;
-fx-background-color: -fixed-log-warn;
}
.log-window-list-cell:info {
-fx-background-color: #FFFFFF;
-fx-text-fill: -fixed-log-text-fill;
-fx-background-color: -fixed-log-info;
}
.log-window-list-cell:debug {
-fx-background-color: #EEE9E0;
-fx-text-fill: -fixed-log-text-fill;
-fx-background-color: -fixed-log-debug;
}
.log-window-list-cell:trace {
-fx-background-color: #EEE9E0;
-fx-text-fill: -fixed-log-text-fill;
-fx-background-color: -fixed-log-trace;
}
.log-window-list-cell:selected {
-fx-background-color: #C4C4C4;
-fx-text-fill: -fixed-log-text-fill;
-fx-background-color: -fixed-log-selected;
}
/*******************************************************************************
@@ -1360,6 +1432,10 @@
-fx-fill: -monet-on-surface;
}
.jfx-combo-box .text-field {
-fx-text-fill: -monet-on-surface;
}
.combo-box-popup .list-view {
-fx-background-color: -monet-surface-container;
}
@@ -1405,17 +1481,6 @@
-fx-min-height: 25px;
}
.jfx-color-picker > .jfx-rippler > .color-box {
-fx-background-color: -monet-primary-seed;
-fx-background-radius: 3px;
-fx-background-insets: 0px;
}
.jfx-color-picker > .jfx-rippler > .color-box:disabled {
-fx-background-color: transparent;
}
.color-palette-region {
}
@@ -1439,6 +1504,75 @@
-fx-max-width: 300;
}
.custom-color-dialog .tab .tab-label {
-fx-text-fill: -monet-on-surface;
}
.custom-color-dialog .tab .jfx-rippler {
-jfx-rippler-fill: -monet-on-surface;
}
.custom-color-dialog .tab-selected-line {
-fx-background-color: -monet-on-surface;
}
/*******************************************************************************
* *
* Table View *
* *
******************************************************************************/
/* WE DEFINITELY NEED JFX TABLE-VIEW */
.table-view {
-fx-padding: 3;
-fx-background-color: -monet-surface-container-low;
}
.table-view,
.table-view .column-header,
.table-view .filler,
.table-view .column-header-background,
.table-row-cell {
-fx-background-radius: 6;
-fx-border-radius: 6;
}
.table-view .column-header,
.table-view .filler {
-fx-background-color: transparent;
}
.table-view .column-header-background {
-fx-background-color: -monet-surface-container-highest;
}
.table-row-cell {
-fx-background-color: -monet-surface-container;
-fx-table-cell-border-color: -monet-surface-container-lowest;
}
.table-row-cell:odd {
-fx-background-color: -monet-surface-container-high;
}
.table-cell {
-fx-background-color: transparent;
-fx-text-fill: -monet-on-surface;
}
.table-view > .virtual-flow > .clipped-container > .sheet > .table-row-cell:filled:selected {
-fx-background-color: -monet-secondary-container;
}
.table-view > .virtual-flow > .scroll-bar {
-fx-skin: "org.jackhuang.hmcl.ui.construct.FloatScrollBarSkin";
}
.table-view > .virtual-flow > .scroll-bar .track {
-fx-fill: transparent;
}
/*******************************************************************************
* *
* JFX Decorator *
@@ -1454,14 +1588,9 @@
}
.jfx-decorator-title {
-fx-text-fill: -monet-on-primary-container;
-fx-font-size: 14;
}
.jfx-decorator-title:transparent {
-fx-text-fill: black;
}
.window {
-fx-background-color: transparent;
-fx-padding: 8;
@@ -1485,7 +1614,6 @@
* Scroll Bar *
* *
******************************************************************************/
/* unsued? */
.scroll-bar:vertical > .track-background, .scroll-bar:horizontal > .track-background {
-fx-background-color: #F1F1F1;
@@ -1518,6 +1646,14 @@
-fx-shape: "M298 598l214-214 214 214h-428z";
}
.jfx-combo-box .scroll-bar {
-fx-skin: "org.jackhuang.hmcl.ui.construct.FloatScrollBarSkin";
}
.jfx-combo-box .scroll-bar .track {
-fx-fill: transparent;
}
/*******************************************************************************
* *
* Scroll Pane *
@@ -1598,7 +1734,7 @@
}
.html-hyperlink {
-fx-fill: blue;
-fx-fill: -monet-primary;
}
.html-h1 {
@@ -1620,3 +1756,18 @@
.html-italic {
-fx-font-style: italic;
}
/*******************************************************************************
* *
* Tooltip *
* *
******************************************************************************/
.tooltip {
-fx-text-fill: -monet-inverse-on-surface;
-fx-background-color: -monet-inverse-surface;
}
.tooltip .text {
-fx-fill: -monet-inverse-on-surface;
}
@@ -197,7 +197,15 @@ button.select_all=Select All
button.view=View
button.yes=Yes
chat=Join Group Chat
contact=Feedback
contact.chat=Join Group Chat
contact.chat.discord=Discord
contact.chat.discord.statement=Welcome to join our Discord server.
contact.chat.qq_group=HMCL User QQ Group
contact.chat.qq_group.statement=Welcome to join our user QQ group.
contact.feedback=Feedback Channel
contact.feedback.github=GitHub Issues
contact.feedback.github.statement=Submit an issue on GitHub.
color.recent=Recommended
color.custom=Custom Color
@@ -376,6 +384,7 @@ extension.mod=Mod File
extension.png=Image File
extension.ps1=Windows PowerShell Script
extension.sh=Shell Script
extension.command=macOS Shell Script
fatal.create_hmcl_current_directory_failure=Hello Minecraft! Launcher cannot create the HMCL directory (%s). Please move HMCL to another location and reopen it.
fatal.javafx.incompatible=Missing JavaFX environment.\n\
@@ -432,15 +441,6 @@ fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher has provided
If you are using the <b>Qualcomm</b> platform, you may need to install the <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL Compatibility Pack</a> before playing games.\n\
Click the link to navigate to the Microsoft Store and install the compatibility pack.
feedback=Feedback
feedback.channel=Feedback Channel
feedback.discord=Discord
feedback.discord.statement=Welcome to join our Discord server.
feedback.github=GitHub Issues
feedback.github.statement=Submit an issue on GitHub.
feedback.qq_group=HMCL User QQ Group
feedback.qq_group.statement=Welcome to join our user QQ group.
file=File
folder.config=Configs
@@ -1207,6 +1207,11 @@ repositories.chooser=HMCL requires JavaFX to work.\n\
repositories.chooser.title=Choose download source for JavaFX
resourcepack=Resource Packs
resourcepack.add=Add
resourcepack.manage=Resource Packs
resourcepack.download=Download
resourcepack.add.failed=Failed to add resource pack
resourcepack.delete.failed=Failed to delete resource pack
resourcepack.download.title=Download Resource Pack - %1s
reveal.in_file_manager=Reveal in File Manager
@@ -1407,6 +1412,7 @@ settings.memory.unit.mib=MiB
settings.memory.used_per_total=%1$.1f GiB Used / %2$.1f GiB Total
settings.physical_memory=Physical Memory Size
settings.show_log=Show Logs
settings.enable_debug_log_output=Output debug log
settings.tabs.installers=Loaders
settings.take_effect_after_restart=Applies After Restart
settings.type=Settings Type of Instance
@@ -1532,7 +1538,7 @@ update.latest=This is the latest version
update.no_browser=Cannot open in system browser. But we copied the link to your clipboard, and you can open it manually.
update.tooltip=Update
update.preview=Preview HMCL releases early
update.preview.tooltip=Enable this option to receive new versions of HMCL early for testing before their official release.
update.preview.subtitle=Enable this option to receive new versions of HMCL early for testing before their official release.
version=Games
version.name=Instance Name
@@ -1553,6 +1559,7 @@ version.game.support_status.unsupported=Unsupported
version.game.support_status.untested=Untested
version.game.type=Type
version.launch=Launch Game
version.launch_and_enter_world=Play World
version.launch.empty=Start Game
version.launch.empty.installing=Installing Game
version.launch.empty.tooltip=Install and launch the latest official release
@@ -197,7 +197,15 @@ button.select_all=تحديد الكل
button.view=عرض
button.yes=نعم
chat=الانضمام إلى دردشة جماعية
contact=الملاحظات
contact.chat=الانضمام إلى دردشة جماعية
contact.chat.discord=Discord
contact.chat.discord.statement=مرحباً بك للانضمام إلى خادم Discord الخاص بنا.
contact.chat.qq_group=مجموعة QQ لمستخدمي HMCL
contact.chat.qq_group.statement=مرحباً بك للانضمام إلى مجموعة QQ الخاصة بمستخدمينا.
contact.feedback=قناة الملاحظات
contact.feedback.github=GitHub Issues
contact.feedback.github.statement=إرسال مشكلة على GitHub.
color.recent=موصى به
color.custom=لون مخصص
@@ -432,15 +440,6 @@ fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher وفر دعم
إذا كنت تستخدم منصة <b>Qualcomm</b>، فقد تحتاج إلى تثبيت <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">حزمة توافق OpenGL</a> قبل لعب الألعاب.\n\
انقر على الرابط للانتقال إلى متجر Microsoft وتثبيت حزمة التوافق.
feedback=الملاحظات
feedback.channel=قناة الملاحظات
feedback.discord=Discord
feedback.discord.statement=مرحباً بك للانضمام إلى خادم Discord الخاص بنا.
feedback.github=GitHub Issues
feedback.github.statement=إرسال مشكلة على GitHub.
feedback.qq_group=مجموعة QQ لمستخدمي HMCL
feedback.qq_group.statement=مرحباً بك للانضمام إلى مجموعة QQ الخاصة بمستخدمينا.
file=ملف
folder.config=التكوينات
@@ -1526,7 +1525,7 @@ update.latest=هذا هو أحدث إصدار
update.no_browser=لا يمكن الفتح في متصفح النظام. ولكننا نسخنا الرابط إلى الحافظة الخاصة بك، ويمكنك فتحه يدويًا.
update.tooltip=تحديث
update.preview=معاينة إصدارات HMCL مبكرًا
update.preview.tooltip=قم بتفعيل هذا الخيار لاستلام إصدارات جديدة من HMCL مبكرًا للاختبار قبل إصدارها الرسمي.
update.preview.subtitle=قم بتفعيل هذا الخيار لاستلام إصدارات جديدة من HMCL مبكرًا للاختبار قبل إصدارها الرسمي.
version=الألعاب
version.name=اسم النسخة
@@ -198,7 +198,15 @@ button.select_all=Seleccionar todo
button.view=Vista
button.yes=Sí
chat=Chat de grupo
contact=Comentarios
contact.chat=Chat de grupo
contact.chat.discord=Discord
contact.chat.discord.statement=¡Únase a nuestro servidor Discord!
contact.chat.qq_group=Grupo QQ de usuarios de HMCL
contact.chat.qq_group.statement=¡Únase a nuestro grupo QQ de usuarios!
contact.feedback=Canal de comentarios
contact.feedback.github=GitHub Issues
contact.feedback.github.statement=Envíe una propuesta en GitHub.
color.recent=Recomendado
color.custom=Color personalizado
@@ -433,15 +441,6 @@ fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher ha proporcion
Si utilizas la plataforma <b>Qualcomm</b>, es posible que tengas que instalar el <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">paquete de compatibilidad OpenGL</a> antes de jugar.\n\
Haz clic en el enlace para ir a Microsoft Store e instalar el paquete de compatibilidad.
feedback=Comentarios
feedback.channel=Canal de comentarios
feedback.discord=Discord
feedback.discord.statement=¡Únase a nuestro servidor Discord!
feedback.github=GitHub Issues
feedback.github.statement=Envíe una propuesta en GitHub.
feedback.qq_group=Grupo QQ de usuarios de HMCL
feedback.qq_group.statement=¡Únase a nuestro grupo QQ de usuarios!
file=Archivo
folder.config=Configuraciones de mod
@@ -1447,7 +1446,7 @@ update.latest=Esta es la última versión.
update.no_browser=No se puede abrir en el navegador del sistema. Pero, hemos copiado el enlace a su portapapeles y puede abrirlo manualmente.
update.tooltip=Actualización
update.preview=Vista previa de actualizaciones anticipadas
update.preview.tooltip=Activa esta opción para recibir nuevas versiones del lanzador antes de su lanzamiento oficial para probarlas
update.preview.subtitle=Activa esta opción para recibir nuevas versiones del lanzador antes de su lanzamiento oficial para probarlas
version=Juegos
version.name=Nombre de instancia
@@ -157,7 +157,15 @@ button.select_all=すべて選択
button.view=読む
button.yes=はい
chat=グループチャット
contact=フィードバック
contact.chat=グループチャット
contact.chat.discord=Discord
contact.chat.discord.statement=Discordサーバーに参加してください!
contact.chat.qq_group=HMCLユーザーQQグループ
contact.chat.qq_group.statement=ユーザーQQグループに参加してください!
contact.feedback=フィードバックチャンネル
contact.feedback.github=GitHub Issues
contact.feedback.github.statement=GitHubで問題を送信します。
color.recent=推奨
color.custom=カスタムカラー
@@ -326,16 +334,6 @@ fatal.apply_update_failure=ごめんなさい、Hello Minecraft! Launcher 何か
fatal.samba=If you are trying to run HMCL in a shared folder by Samba, HMCL may not working, please try updating your Java or running HMCL in a local folder.
fatal.illegal_char=ユーザーフォルダーのパスに不正な文字'='が含まれています, ログインアカウントやオフラインログインではスキンの変更ができなくなり。
feedback=フィードバック
feedback.channel=フィードバックチャンネル
feedback.discord=Discord
feedback.discord.statement=Discordサーバーに参加してください!
feedback.github=GitHub Issues
feedback.github.statement=GitHubで問題を送信します。
feedback.qq_group=HMCLユーザーQQグループ
feedback.qq_group.statement=ユーザーQQグループに参加してください!
file=ファイル
folder.config=Config
@@ -206,11 +206,19 @@ button.select_all=悉擇之
button.view=覽
button.yes=然
chat=會集
color.recent=薦
color.custom=自定色
contact=建言
contact.chat=會集
contact.chat.discord=齟齬
contact.chat.discord.statement=恭迎至齟齬伺服器,且循論議之規
contact.chat.qq_group=HMCL 群組
contact.chat.qq_group.statement=恭迎至 HMCL 群組,且循論議之規
contact.feedback=建言之徑
contact.feedback.github=Github 議題
contact.feedback.github.statement=舉一 Github 議題
crash.NoClassDefFound=宜驗 HMCL 之案全否,抑更迭爪哇。\n君可求助於 https://docs.hmcl.net/help.html。
crash.user_fault=君之械網與爪哇或有謬,是以崩。宜驗爪哇與算機。\n君可求助於 https://docs.hmcl.net/help.html。
@@ -401,15 +409,6 @@ fatal.unsupported_platform.loongarch=HMCL 既適龍芯。\n凡有謬,遽求助
fatal.unsupported_platform.macos_arm64=HMCL 既適蘋矽。宜啟以 ARM 之爪哇,以益君之戲事。\n誠有謬,宜啟以 x86-64 之爪哇,以益其兼。\n凡有謬,遽求助於右上之鈕。
fatal.unsupported_platform.windows_arm64=HMCL 既適 ARM 之視窗。誠有謬,宜啟以 x86 之爪哇。\n誠用<b>栝柑</b>,或須置<a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">開圖庫兼囊</a>,而後可戯。擊鏈以置之於微軟貨舍。\n君可求助於右上之鈕。
feedback=建言
feedback.channel=建言之徑
feedback.discord=齟齬
feedback.discord.statement=恭迎至齟齬伺服器,且循論議之規
feedback.github=Github 議題
feedback.github.statement=舉一 Github 議題
feedback.qq_group=HMCL 群組
feedback.qq_group.statement=恭迎至 HMCL 群組,且循論議之規
file=案
folder.config=置設案夾
@@ -197,7 +197,15 @@ button.select_all=Выбрать все
button.view=Просмотреть
button.yes=Да
chat=Групповой чат
contact=Обратная связь
contact.chat=Групповой чат
contact.chat.discord=Discord
contact.chat.discord.statement=Добро пожаловать в наш Discord.
contact.chat.qq_group=Группа QQ пользователя HMCL
contact.chat.qq_group.statement=Добро пожаловать в нашу группу QQ.
contact.feedback=Канал обратной связи
contact.feedback.github=Проблемы GitHub
contact.feedback.github.statement=Отправить проблему на GitHub.
color.recent=Рекомендуемые
color.custom=Пользовательский цвет
@@ -429,15 +437,6 @@ fatal.unsupported_platform.windows_arm64=Лаунчер обеспечил на
Если вы используете платформу <b>Qualcomm</b>, вам может потребоваться установить <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">пакет совместимости OpenGL</a> перед началом игры.\n\
Щелкните ссылку, чтобы перейти в Microsoft Store и установить пакет совместимости.
feedback=Обратная связь
feedback.channel=Канал обратной связи
feedback.discord=Discord
feedback.discord.statement=Добро пожаловать в наш Discord.
feedback.github=Проблемы GitHub
feedback.github.statement=Отправить проблему на GitHub.
feedback.qq_group=Группа QQ пользователя HMCL
feedback.qq_group.statement=Добро пожаловать в нашу группу QQ.
file=Файл
folder.config=Конфигурация мод
@@ -194,7 +194,15 @@ button.select_all=Вибрати все
button.view=Переглянути
button.yes=Так
chat=Приєднатися до групового чату
contact=Зворотний зв'язок
contact.chat=Приєднатися до групового чату
contact.chat.discord=Discord
contact.chat.discord.statement=Ласкаво просимо приєднатися до нашого сервера Discord.
contact.chat.qq_group=Група користувачів QQ HMCL
contact.chat.qq_group.statement=Ласкаво просимо приєднатися до нашої групи користувачів QQ.
contact.feedback=Канал зворотного зв'язку
contact.feedback.github=GitHub Issues
contact.feedback.github.statement=Надіслати проблему на GitHub.
color.recent=Рекомендовані
color.custom=Власний колір
@@ -411,15 +419,6 @@ fatal.unsupported_platform.windows_arm64=Hello Minecraft! Лаунчер над
Якщо ви використовуєте платформу <b>Qualcomm</b>, вам може знадобитися встановити <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">Пакет сумісності OpenGL</a> перед грою.\n\
Натисніть посилання, щоб перейти до Microsoft Store та встановити пакет сумісності.
feedback=Зворотний зв'язок
feedback.channel=Канал зворотного зв'язку
feedback.discord=Discord
feedback.discord.statement=Ласкаво просимо приєднатися до нашого сервера Discord.
feedback.github=GitHub Issues
feedback.github.statement=Надіслати проблему на GitHub.
feedback.qq_group=Група користувачів QQ HMCL
feedback.qq_group.statement=Ласкаво просимо приєднатися до нашої групи користувачів QQ.
file=Файл
folder.config=Конфігурації
@@ -200,7 +200,15 @@ button.select_all=全選
button.view=查看
button.yes=是
chat=官方群組
contact=回報
contact.chat=官方群組
contact.chat.discord=Discord 伺服器
contact.chat.discord.statement=歡迎加入 Discord 伺服器,加入後請遵守討論區規則
contact.chat.qq_group=使用者 QQ 群組
contact.chat.qq_group.statement=歡迎加入 HMCL 使用者 QQ 群組,加入後請遵守群組規則
contact.feedback=回報管道
contact.feedback.github=GitHub Issues
contact.feedback.github.statement=提交一個 GitHub Issue
color.recent=建議
color.custom=自訂顏色
@@ -374,6 +382,7 @@ extension.mod=模組檔案
extension.png=圖片檔案
extension.ps1=PowerShell 指令碼
extension.sh=Bash 指令碼
extension.command=macOS Shell 指令碼
fatal.create_hmcl_current_directory_failure=Hello Minecraft! Launcher 無法建立 HMCL 資料夾 (%s),請將 HMCL 移動至其他位置再開啟。
fatal.javafx.incompatible=缺少 JavaFX 執行環境。\nHMCL 無法在低於 Java 11 的 Java 環境上自行補全 JavaFX 執行環境。請更新到 Java 11 或更高版本。
@@ -398,15 +407,6 @@ fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher 已為龍芯提
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Launcher 已為 Apple Silicon 平臺提供支援。使用 ARM 原生 Java 啟動遊戲以獲得更流暢的遊戲體驗。\n如果你在遊戲中遭遇問題,使用 x86-64 架構的 Java 啟動遊戲可能有更好的相容性。
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher 已為 Windows on Arm 平臺提供原生支援。如果你在遊戲中遭遇問題,請嘗試使用 x86 架構的 Java 啟動遊戲。\n\n如果你正在使用<b>高通</b>平臺,你可能需要安裝 <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL 相容包</a>後才能進行遊戲。點擊連結前往 Microsoft Store 安裝相容包。
feedback=回報
feedback.channel=回報管道
feedback.discord=Discord 伺服器
feedback.discord.statement=歡迎加入 Discord 伺服器,加入後請遵守討論區規則
feedback.github=GitHub Issues
feedback.github.statement=提交一個 GitHub Issue
feedback.qq_group=HMCL 使用者 QQ 群組
feedback.qq_group.statement=歡迎加入 HMCL 使用者 QQ 群組,加入後請遵守群組規則
file=檔案
folder.config=模組設定目錄
@@ -1000,6 +1000,11 @@ repositories.chooser=缺少 JavaFX 執行環境。HMCL 需要 JavaFX 才能正
repositories.chooser.title=選取 JavaFX 下載源
resourcepack=資源包
resourcepack.add=新增資源包
resourcepack.manage=資源包管理
resourcepack.download=下載資源包
resourcepack.add.failed=新增資源包失敗
resourcepack.delete.failed=刪除資源包失敗
resourcepack.download.title=資源包下載 - %1s
reveal.in_file_manager=在檔案管理員中查看
@@ -1195,6 +1200,7 @@ settings.memory.unit.mib=MiB
settings.memory.used_per_total=已使用 %1$.1f GiB / 總記憶體 %2$.1f GiB
settings.physical_memory=實體記憶體大小
settings.show_log=查看日誌
settings.enable_debug_log_output=輸出除錯日誌
settings.tabs.installers=自動安裝
settings.take_effect_after_restart=重啟後生效
settings.type=實例遊戲設定類型
@@ -1316,7 +1322,7 @@ update.latest=目前版本為最新版本
update.no_browser=無法開啟瀏覽器。網址已經複製到剪貼簿了,你可以手動複製網址開啟頁面。
update.tooltip=更新
update.preview=提前測試 HMCL 預覽版本
update.preview.tooltip=啟用此選項,你將可以提前取得 HMCL 的新版本,以便在正式發布前進行測試。
update.preview.subtitle=啟用此選項,你將可以提前取得 HMCL 的新版本,以便在正式發布前進行測試。
version=遊戲
version.name=遊戲實例名稱
@@ -1337,6 +1343,7 @@ version.game.support_status.unsupported=不支援
version.game.support_status.untested=未經測試
version.game.type=版本類型
version.launch=啟動遊戲
version.launch_and_enter_world=進入世界
version.launch.empty=開始遊戲
version.launch.empty.installing=安裝遊戲
version.launch.empty.tooltip=安裝並啟動最新正式版遊戲
@@ -208,11 +208,19 @@ button.select_all=全选
button.view=查看
button.yes=是
chat=官方群组
color.recent=推荐
color.custom=自定义颜色
contact=反馈
contact.feedback=提交反馈
contact.feedback.github=GitHub Issue
contact.feedback.github.statement=提交一个 GitHub Issue
contact.chat=官方群组
contact.chat.qq_group=用户 QQ 群
contact.chat.qq_group.statement=欢迎加入 HMCL 官方 QQ 群,加入后请遵守群规
contact.chat.discord=Discord
contact.chat.discord.statement=欢迎加入 Discord 服务器,加入后请遵守讨论区规定
crash.NoClassDefFound=请确认 Hello Minecraft! Launcher 本体是否完整,或更新你的 Java。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
crash.user_fault=你的系统或 Java 环境可能安装不当导致本软件崩溃,请检查你的 Java 环境或你的电脑。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
@@ -383,6 +391,7 @@ extension.mod=模组文件
extension.png=图片文件
extension.ps1=PowerShell 脚本
extension.sh=Bash 脚本
extension.command=macOS Shell 脚本
fatal.create_hmcl_current_directory_failure=Hello Minecraft! Launcher 无法创建 HMCL 文件夹 (%s),请将 HMCL 移动至其他位置再启动。\n如遇到问题,你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
fatal.javafx.incompatible=缺少 JavaFX 运行环境。\nHello Minecraft! Launcher 无法在低于 Java 11 的 Java 环境上自行补全 JavaFX 运行环境。请更新到 Java 11 或更高版本。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
@@ -407,15 +416,6 @@ fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher 已为龙芯提
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Launcher 已为 Apple Silicon 平台提供支持。使用 ARM 原生 Java 启动游戏以获得更流畅的游戏体验。\n如果你在游戏中遇到问题,使用 x86-64 架构的 Java 启动游戏可能有更好的兼容性。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher 已为 Windows on Arm 平台提供原生支持。如果你在游戏中遇到问题,请尝试使用 x86 架构的 Java 启动游戏。\n如果你正在使用 <b>高通</b> 平台,你可能需要安装 <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL 兼容包</a> 后才能进行游戏。点击链接前往 Microsoft Store 安装兼容包。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
feedback=反馈
feedback.channel=反馈渠道
feedback.discord=Discord
feedback.discord.statement=欢迎加入 Discord 服务器,加入后请遵守讨论区规定
feedback.github=GitHub Issue
feedback.github.statement=提交一个 GitHub Issue
feedback.qq_group=HMCL 用户群
feedback.qq_group.statement=欢迎加入 HMCL 用户群,加入后请遵守群规
file=文件
folder.config=配置文件夹
@@ -1010,6 +1010,11 @@ repositories.chooser=缺少 JavaFX 运行环境。HMCL 需要 JavaFX 才能正
repositories.chooser.title=选择 JavaFX 下载源
resourcepack=资源包
resourcepack.add=添加资源包
resourcepack.manage=资源包管理
resourcepack.download=下载资源包
resourcepack.add.failed=添加资源包失败
resourcepack.delete.failed=删除资源包失败
resourcepack.download.title=资源包下载 - %1s
reveal.in_file_manager=在文件管理器中查看
@@ -1205,6 +1210,7 @@ settings.memory.unit.mib=MiB
settings.memory.used_per_total=设备中已使用 %1$.1f GiB / 设备总内存 %2$.1f GiB
settings.physical_memory=物理内存大小
settings.show_log=查看日志
settings.enable_debug_log_output=输出调试日志
settings.tabs.installers=自动安装
settings.take_effect_after_restart=重启后生效
settings.type=实例游戏设置类型
@@ -1326,7 +1332,7 @@ update.latest=当前版本为最新版本
update.no_browser=无法打开浏览器。网址已经复制到剪贴板,你可以手动粘贴网址打开页面。
update.tooltip=更新
update.preview=提前预览 HMCL 版本
update.preview.tooltip=启用此选项,你将可以提前获取 HMCL 的新版本,以便在正式发布前进行测试。
update.preview.subtitle=启用此选项,你将可以提前获取 HMCL 的新版本,以便在正式发布前进行测试。
version=游戏
version.name=游戏实例名称
@@ -1347,6 +1353,7 @@ version.game.support_status.unsupported=不支持
version.game.support_status.untested=未经测试
version.game.type=版本类型
version.launch=启动游戏
version.launch_and_enter_world=进入世界
version.launch.empty=开始游戏
version.launch.empty.installing=安装游戏
version.launch.empty.tooltip=安装并启动最新正式版游戏
@@ -103,10 +103,14 @@ public class OfflineAccount extends Account {
return skin != null && skin.getType() != Skin.Type.DEFAULT;
}
public AuthInfo logInWithoutSkin() throws AuthenticationException {
// Using "legacy" user type here because "mojang" user type may cause "invalid session token" or "disconnected" when connecting to a game server.
return new AuthInfo(username, uuid, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), AuthInfo.USER_TYPE_MSA, "{}");
}
@Override
public AuthInfo logIn() throws AuthenticationException {
// Using "legacy" user type here because "mojang" user type may cause "invalid session token" or "disconnected" when connecting to a game server.
AuthInfo authInfo = new AuthInfo(username, uuid, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), AuthInfo.USER_TYPE_MSA, "{}");
AuthInfo authInfo = logInWithoutSkin();
if (loadAuthlibInjector(skin)) {
CompletableFuture<AuthlibInjectorArtifactInfo> artifactTask = CompletableFuture.supplyAsync(() -> {
@@ -177,6 +177,6 @@ public final class ForgeInstallTask extends Task<Version> {
}
private static String modifyVersion(String gameVersion, String version) {
return removeSuffix(removePrefix(removeSuffix(removePrefix(version.replace(gameVersion, "").trim(), "-"), "-"), "_"), "_");
return removePrefix(removeSuffix(removePrefix(removeSuffix(removePrefix(version.replace(gameVersion, "").trim(), "-"), "-"), "_"), "_"), "forge-");
}
}
@@ -564,4 +564,8 @@ public class DefaultGameRepository implements GameRepository {
.append("baseDirectory", baseDirectory)
.toString();
}
public Path getResourcepacksDirectory(String id) {
return getRunDirectory(id).resolve("resourcepacks");
}
}
@@ -25,6 +25,7 @@ import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.util.*;
public final class GameJavaVersion {
public static final GameJavaVersion JAVA_25 = new GameJavaVersion("java-runtime-epsilon", 25);
public static final GameJavaVersion JAVA_21 = new GameJavaVersion("java-runtime-delta", 21);
public static final GameJavaVersion JAVA_17 = new GameJavaVersion("java-runtime-beta", 17);
public static final GameJavaVersion JAVA_16 = new GameJavaVersion("java-runtime-alpha", 16);
@@ -33,6 +34,8 @@ public final class GameJavaVersion {
public static final GameJavaVersion LATEST = JAVA_21;
public static GameJavaVersion getMinimumJavaVersion(GameVersionNumber gameVersion) {
if (gameVersion.compareTo("26.1") >= 0)
return JAVA_25;
if (gameVersion.compareTo("1.20.5") >= 0)
return JAVA_21;
if (gameVersion.compareTo("1.18") >= 0)
@@ -45,18 +48,14 @@ public final class GameJavaVersion {
}
public static GameJavaVersion get(int major) {
switch (major) {
case 8:
return JAVA_8;
case 16:
return JAVA_16;
case 17:
return JAVA_17;
case 21:
return JAVA_21;
default:
return null;
}
return switch (major) {
case 8 -> JAVA_8;
case 16 -> JAVA_16;
case 17 -> JAVA_17;
case 21 -> JAVA_21;
case 25 -> JAVA_25;
default -> null;
};
}
public static List<GameJavaVersion> getSupportedVersions(Platform platform) {
@@ -74,13 +73,13 @@ public final class GameJavaVersion {
case WINDOWS:
case LINUX:
case MACOS:
return Arrays.asList(JAVA_8, JAVA_16, JAVA_17, JAVA_21);
return Arrays.asList(JAVA_8, JAVA_16, JAVA_17, JAVA_21, JAVA_25);
}
} else if (architecture == Architecture.ARM64) {
switch (operatingSystem) {
case WINDOWS:
case MACOS:
return Arrays.asList(JAVA_17, JAVA_21);
return Arrays.asList(JAVA_17, JAVA_21, JAVA_25);
}
}
@@ -47,7 +47,7 @@ public class LaunchOptions implements Serializable {
private Integer width;
private Integer height;
private boolean fullscreen;
private String serverIp;
private QuickPlayOption quickPlayOption;
private String wrapper;
private Proxy.Type proxyType;
private String proxyHost;
@@ -64,6 +64,7 @@ public class LaunchOptions implements Serializable {
private Renderer renderer = Renderer.DEFAULT;
private boolean useNativeGLFW;
private boolean useNativeOpenAL;
private boolean enableDebugLogOutput;
private boolean daemon;
/**
@@ -180,11 +181,11 @@ public class LaunchOptions implements Serializable {
return fullscreen;
}
/**
* The server ip that will connect to when enter game main menu.
*/
public String getServerIp() {
return serverIp;
/// The quick play option.
///
/// @see <a href="https://minecraft.wiki/w/Quick_Play">Quick Play - Minecraft Wiki</a>
public QuickPlayOption getQuickPlayOption() {
return quickPlayOption;
}
/**
@@ -282,6 +283,10 @@ public class LaunchOptions implements Serializable {
return useNativeOpenAL;
}
public boolean isEnableDebugLogOutput() {
return enableDebugLogOutput;
}
/**
* Will launcher keeps alive after game launched or not.
*/
@@ -407,8 +412,8 @@ public class LaunchOptions implements Serializable {
return this;
}
public Builder setServerIp(String serverIp) {
options.serverIp = serverIp;
public Builder setQuickPlayOption(QuickPlayOption quickPlayOption) {
options.quickPlayOption = quickPlayOption;
return this;
}
@@ -497,5 +502,9 @@ public class LaunchOptions implements Serializable {
return this;
}
public Builder setEnableDebugLogOutput(boolean u) {
options.enableDebugLogOutput = u;
return this;
}
}
}
@@ -0,0 +1,32 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
/// The quick play option.
///
/// @see <a href="https://minecraft.wiki/w/Quick_Play">Quick Play - Minecraft Wiki</a>
public sealed interface QuickPlayOption {
record SinglePlayer(String worldFolderName) implements QuickPlayOption {
}
record MultiPlayer(String serverIP) implements QuickPlayOption {
}
record Realm(String realmID) implements QuickPlayOption {
}
}
@@ -24,6 +24,7 @@ import com.github.steveice10.opennbt.tag.builtin.StringTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.util.io.*;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
@@ -48,7 +49,7 @@ public final class World {
private final Path file;
private String fileName;
private String worldName;
private String gameVersion;
private GameVersionNumber gameVersion;
private long lastPlayed;
private Image icon;
private Long seed;
@@ -108,7 +109,7 @@ public final class World {
return lastPlayed;
}
public String getGameVersion() {
public @Nullable GameVersionNumber getGameVersion() {
return gameVersion;
}
@@ -188,7 +189,7 @@ public final class World {
CompoundTag version = data.get("Version");
if (version.get("Name") instanceof StringTag)
gameVersion = version.<StringTag>get("Name").getValue();
gameVersion = GameVersionNumber.asGameVersion(version.<StringTag>get("Name").getValue());
}
Tag worldGenSettings = data.get("WorldGenSettings");
@@ -149,7 +149,7 @@ public class DefaultLauncher extends Launcher {
res.addDefault("-Dcom.sun.jndi.cosnaming.object.trustURLCodebase=", "false");
String formatMsgNoLookups = res.addDefault("-Dlog4j2.formatMsgNoLookups=", "true");
if (!"-Dlog4j2.formatMsgNoLookups=false".equals(formatMsgNoLookups) && isUsingLog4j()) {
if (isUsingLog4j() && (options.isEnableDebugLogOutput() || !"-Dlog4j2.formatMsgNoLookups=false".equals(formatMsgNoLookups))) {
res.addDefault("-Dlog4j.configurationFile=", FileUtils.getAbsolutePath(getLog4jConfigurationFile()));
}
@@ -304,23 +304,31 @@ public class DefaultLauncher extends Launcher {
if (argumentsFromAuthInfo != null && argumentsFromAuthInfo.getGame() != null && !argumentsFromAuthInfo.getGame().isEmpty())
res.addAll(Arguments.parseArguments(argumentsFromAuthInfo.getGame(), configuration, features));
if (StringUtils.isNotBlank(options.getServerIp())) {
String address = options.getServerIp();
if (options.getQuickPlayOption() instanceof QuickPlayOption.MultiPlayer multiPlayer) {
String address = multiPlayer.serverIP();
try {
ServerAddress parsed = ServerAddress.parse(address);
if (GameVersionNumber.asGameVersion(gameVersion).compareTo("1.20") < 0) {
if (GameVersionNumber.asGameVersion(gameVersion).isAtLeast("1.20", "23w14a")) {
res.add("--quickPlayMultiplayer");
res.add(parsed.getPort() >= 0 ? address : parsed.getHost() + ":25565");
} else {
res.add("--server");
res.add(parsed.getHost());
res.add("--port");
res.add(parsed.getPort() >= 0 ? String.valueOf(parsed.getPort()) : "25565");
} else {
res.add("--quickPlayMultiplayer");
res.add(parsed.getPort() < 0 ? address + ":25565" : address);
}
} catch (IllegalArgumentException e) {
LOG.warning("Invalid server address: " + address, e);
}
} else if (options.getQuickPlayOption() instanceof QuickPlayOption.SinglePlayer singlePlayer
&& GameVersionNumber.asGameVersion(gameVersion).isAtLeast("1.20", "23w14a")) {
res.add("--quickPlaySingleplayer");
res.add(singlePlayer.worldFolderName());
} else if (options.getQuickPlayOption() instanceof QuickPlayOption.Realm realm
&& GameVersionNumber.asGameVersion(gameVersion).isAtLeast("1.20", "23w14a")) {
res.add("--quickPlayRealms");
res.add(realm.realmID());
}
if (options.isFullscreen())
@@ -421,14 +429,24 @@ public class DefaultLauncher extends Launcher {
public void extractLog4jConfigurationFile() throws IOException {
Path targetFile = getLog4jConfigurationFile();
InputStream source;
String sourcePath;
if (GameVersionNumber.asGameVersion(repository.getGameVersion(version)).compareTo("1.12") < 0) {
source = DefaultLauncher.class.getResourceAsStream("/assets/game/log4j2-1.7.xml");
if (options.isEnableDebugLogOutput()) {
sourcePath = "/assets/game/log4j2-1.7-debug.xml";
} else {
sourcePath = "/assets/game/log4j2-1.7.xml";
}
} else {
source = DefaultLauncher.class.getResourceAsStream("/assets/game/log4j2-1.12.xml");
if (options.isEnableDebugLogOutput()) {
sourcePath = "/assets/game/log4j2-1.12-debug.xml";
} else {
sourcePath = "/assets/game/log4j2-1.12.xml";
}
}
try (InputStream input = source) {
try (InputStream input = DefaultLauncher.class.getResourceAsStream(sourcePath)) {
Files.copy(input, targetFile, StandardCopyOption.REPLACE_EXISTING);
}
}
@@ -609,8 +627,8 @@ public class DefaultLauncher extends Launcher {
if (!usePowerShell) {
if (isWindows && !scriptExtension.equals("bat"))
throw new IllegalArgumentException("The extension of " + scriptFile + " is not 'bat' or 'ps1' in Windows");
else if (!isWindows && !scriptExtension.equals("sh"))
throw new IllegalArgumentException("The extension of " + scriptFile + " is not 'sh' or 'ps1' in macOS/Linux");
else if (!isWindows && !(scriptExtension.equals("sh") || scriptExtension.equals("command")))
throw new IllegalArgumentException("The extension of " + scriptFile + " is not 'sh', 'ps1' or 'command' in macOS/Linux");
}
final Command commandLine = generateCommandLine(nativeFolder);
@@ -219,7 +219,7 @@ public class Datapack {
private Optional<Pack> parsePack(Path datapackPath, boolean isDirectory, String name, Path mcmetaPath) {
try {
PackMcMeta mcMeta = JsonUtils.fromNonNullJson(Files.readString(mcmetaPath), PackMcMeta.class);
return Optional.of(new Pack(datapackPath, isDirectory, name, mcMeta.getPackInfo().getDescription(), this));
return Optional.of(new Pack(datapackPath, isDirectory, name, mcMeta.pack().description(), this));
} catch (JsonParseException e) {
LOG.warning("Invalid pack.mcmeta format in " + datapackPath, e);
} catch (IOException e) {
@@ -169,6 +169,9 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
}
long hash = Integer.toUnsignedLong(MurmurHash2.hash32(baos.toByteArray(), baos.size(), 1));
if (hash == 811513880) { // Workaround for https://github.com/HMCL-dev/HMCL/issues/4597
return Optional.empty();
}
Response<FingerprintMatchesResult> response = withApiKey(HttpRequest.POST(PREFIX + "/v1/fingerprints/432"))
.json(mapOf(pair("fingerprints", Collections.singletonList(hash))))
@@ -1,6 +1,12 @@
package org.jackhuang.hmcl.mod.modinfo;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.annotations.JsonAdapter;
import com.moandjiezana.toml.Toml;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.ModLoaderType;
@@ -13,10 +19,15 @@ import org.jackhuang.hmcl.util.io.CompressingUtils;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.lang.reflect.Type;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.StringJoiner;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
@@ -68,6 +79,7 @@ public final class ForgeNewModMetadata {
private final String displayName;
private final String side;
private final String displayURL;
@JsonAdapter(AuthorDeserializer.class)
private final String authors;
private final String description;
@@ -112,6 +124,29 @@ public final class ForgeNewModMetadata {
public String getDescription() {
return description;
}
static final class AuthorDeserializer implements JsonDeserializer<String> {
@Override
public String deserialize(JsonElement authors, Type type, JsonDeserializationContext context) throws JsonParseException {
if (authors == null || authors.isJsonNull()) {
return null;
} else if (authors instanceof JsonPrimitive primitive) {
return primitive.getAsString();
} else if (authors instanceof JsonArray array) {
var joiner = new StringJoiner(", ");
for (int i = 0; i < array.size(); i++) {
if (!(array.get(i) instanceof JsonPrimitive element)) {
return authors.toString();
}
joiner.add(element.getAsString());
}
return joiner.toString();
}
return authors.toString();
}
}
}
public static LocalModFile fromForgeFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException {
@@ -23,9 +23,9 @@ import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.ModLoaderType;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonSerializable;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.gson.Validation;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -36,28 +36,12 @@ import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@Immutable
public class PackMcMeta implements Validation {
@SerializedName("pack")
private final PackInfo pack;
public PackMcMeta() {
this(new PackInfo());
}
public PackMcMeta(PackInfo packInfo) {
this.pack = packInfo;
}
public PackInfo getPackInfo() {
return pack;
}
@JsonSerializable
public record PackMcMeta(@SerializedName("pack") PackInfo pack) implements Validation {
@Override
public void validate() throws JsonParseException {
if (pack == null)
@@ -65,29 +49,10 @@ public class PackMcMeta implements Validation {
}
@JsonAdapter(PackInfoDeserializer.class)
public static class PackInfo {
@SerializedName("pack_format")
private final int packFormat;
@SerializedName("min_format")
private final PackVersion minPackVersion;
@SerializedName("max_format")
private final PackVersion maxPackVersion;
@SerializedName("description")
private final LocalModFile.Description description;
public PackInfo() {
this(0, PackVersion.UNSPECIFIED, PackVersion.UNSPECIFIED, new LocalModFile.Description(Collections.emptyList()));
}
public PackInfo(int packFormat, PackVersion minPackVersion, PackVersion maxPackVersion, LocalModFile.Description description) {
this.packFormat = packFormat;
this.minPackVersion = minPackVersion;
this.maxPackVersion = maxPackVersion;
this.description = description;
}
public record PackInfo(@SerializedName("pack_format") int packFormat,
@SerializedName("min_format") PackVersion minPackVersion,
@SerializedName("max_format") PackVersion maxPackVersion,
@SerializedName("description") LocalModFile.Description description) {
public PackVersion getEffectiveMinVersion() {
return !minPackVersion.isUnspecified() ? minPackVersion : new PackVersion(packFormat, 0);
}
@@ -95,10 +60,6 @@ public class PackMcMeta implements Validation {
public PackVersion getEffectiveMaxVersion() {
return !maxPackVersion.isUnspecified() ? maxPackVersion : new PackVersion(packFormat, 0);
}
public LocalModFile.Description getDescription() {
return description;
}
}
public record PackVersion(int majorVersion, int minorVersion) implements Comparable<PackVersion> {
@@ -148,7 +109,7 @@ public class PackMcMeta implements Validation {
}
}
public static class PackInfoDeserializer implements JsonDeserializer<PackInfo> {
public static final class PackInfoDeserializer implements JsonDeserializer<PackInfo> {
private List<LocalModFile.Description.Part> pairToPart(List<Pair<String, String>> lists, String color) {
List<LocalModFile.Description.Part> parts = new ArrayList<>();
@@ -0,0 +1,30 @@
package org.jackhuang.hmcl.resourcepack;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Locale;
public interface ResourcepackFile {
@Nullable
LocalModFile.Description getDescription();
String getName();
Path getPath();
byte @Nullable [] getIcon();
static ResourcepackFile parse(Path path) throws IOException {
String fileName = path.getFileName().toString();
if (Files.isRegularFile(path) && fileName.toLowerCase(Locale.ROOT).endsWith(".zip")) {
return new ResourcepackZipFile(path);
} else if (Files.isDirectory(path) && Files.exists(path.resolve("pack.mcmeta"))) {
return new ResourcepackFolder(path);
}
return null;
}
}
@@ -0,0 +1,60 @@
package org.jackhuang.hmcl.resourcepack;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.modinfo.PackMcMeta;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class ResourcepackFolder implements ResourcepackFile {
private final Path path;
private final LocalModFile.Description description;
private final byte @Nullable [] icon;
public ResourcepackFolder(Path path) {
this.path = path;
LocalModFile.Description description = null;
try {
description = JsonUtils.fromJsonFile(path.resolve("pack.mcmeta"), PackMcMeta.class).pack().description();
} catch (Exception e) {
LOG.warning("Failed to parse resourcepack meta", e);
}
byte[] icon;
try {
icon = Files.readAllBytes(path.resolve("pack.png"));
} catch (IOException e) {
icon = null;
LOG.warning("Failed to read resourcepack icon", e);
}
this.icon = icon;
this.description = description;
}
@Override
public String getName() {
return path.getFileName().toString();
}
@Override
public Path getPath() {
return path;
}
@Override
public LocalModFile.Description getDescription() {
return description;
}
@Override
public byte @Nullable [] getIcon() {
return icon;
}
}
@@ -0,0 +1,72 @@
package org.jackhuang.hmcl.resourcepack;
import org.jackhuang.hmcl.mod.LocalModFile;
import org.jackhuang.hmcl.mod.modinfo.PackMcMeta;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.tree.ZipFileTree;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class ResourcepackZipFile implements ResourcepackFile {
private final Path path;
private final byte @Nullable [] icon;
private final String name;
private final LocalModFile.Description description;
public ResourcepackZipFile(Path path) throws IOException {
this.path = path;
LocalModFile.Description description = null;
byte[] icon = null;
try (var zipFileTree = new ZipFileTree(CompressingUtils.openZipFile(path))) {
try {
description = JsonUtils.fromNonNullJson(zipFileTree.readTextEntry("/pack.mcmeta"), PackMcMeta.class).pack().description();
} catch (Exception e) {
LOG.warning("Failed to parse resourcepack meta", e);
}
var iconEntry = zipFileTree.getEntry("/pack.png");
if (iconEntry != null) {
try (InputStream is = zipFileTree.getInputStream(iconEntry)) {
icon = is.readAllBytes();
} catch (Exception e) {
LOG.warning("Failed to load resourcepack icon", e);
}
}
}
this.icon = icon;
this.description = description;
name = FileUtils.getNameWithoutExtension(path);
}
@Override
public String getName() {
return name;
}
@Override
public Path getPath() {
return path;
}
@Override
public LocalModFile.Description getDescription() {
return description;
}
@Override
public byte @Nullable [] getIcon() {
return icon;
}
}
@@ -193,7 +193,7 @@ public class FileDownloadTask extends FetchTask<Void> {
if (integrityCheck != null) {
algorithm = integrityCheck.getAlgorithm();
checksum = integrityCheck.getChecksum();
} else if (bmclapiHash != null) {
} else if (bmclapiHash != null && DigestUtils.isSha1Digest(bmclapiHash)) {
algorithm = "SHA-1";
checksum = bmclapiHash;
} else {
@@ -17,8 +17,6 @@
*/
package org.jackhuang.hmcl.util;
import javafx.scene.paint.Color;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -27,30 +25,24 @@ import java.util.regex.Pattern;
* @author huangyuhui
*/
public enum Log4jLevel {
FATAL(1, Color.web("#F7A699")),
ERROR(2, Color.web("#FFCCBB")),
WARN(3, Color.web("#FFEECC")),
INFO(4, Color.web("#FBFBFB")),
DEBUG(5, Color.web("#EEE9E0")),
TRACE(6, Color.BLUE),
ALL(2147483647, Color.BLACK);
FATAL(1),
ERROR(2),
WARN(3),
INFO(4),
DEBUG(5),
TRACE(6),
ALL(2147483647);
private final int level;
private final Color color;
Log4jLevel(int level, Color color) {
Log4jLevel(int level) {
this.level = level;
this.color = color;
}
public int getLevel() {
return level;
}
public Color getColor() {
return color;
}
public boolean lessOrEqual(Log4jLevel level) {
return this.level <= level.level;
}
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.util.io;
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.*;
import java.net.*;
@@ -419,6 +420,15 @@ public final class NetworkUtils {
public static @NotNull URI toURI(@NotNull URL url) {
return toURI(url.toExternalForm());
}
// ====
public static @Nullable URI toURIOrNull(String uri) {
if (StringUtils.isNotBlank(uri)) {
try {
return toURI(uri);
} catch (Exception ignored) {
}
}
return null;
}
}
@@ -113,16 +113,19 @@ public final class ManagedProcess {
*/
public List<String> getLines(Predicate<String> lineFilter) {
lock.lock();
try {
if (lineFilter == null)
return List.copyOf(lines);
if (lineFilter == null)
return List.copyOf(lines);
ArrayList<String> res = new ArrayList<>();
for (String line : this.lines) {
if (lineFilter.test(line))
res.add(line);
ArrayList<String> res = new ArrayList<>();
for (String line : this.lines) {
if (lineFilter.test(line))
res.add(line);
}
return Collections.unmodifiableList(res);
} finally {
lock.unlock();
}
return Collections.unmodifiableList(res);
}
public void addLine(String line) {
@@ -0,0 +1,32 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util.platform.windows;
import com.sun.jna.PointerType;
import com.sun.jna.win32.StdCallLibrary;
import org.jackhuang.hmcl.util.platform.NativeUtils;
/// @author Glavo
public interface Dwmapi extends StdCallLibrary {
Dwmapi INSTANCE = NativeUtils.USE_JNA && com.sun.jna.Platform.isWindows()
? NativeUtils.load("dwmapi", Dwmapi.class)
: null;
/// @see <a href="https://learn.microsoft.com/windows/win32/api/dwmapi/nf-dwmapi-dwmsetwindowattribute">DwmSetWindowAttribute function</a>
int DwmSetWindowAttribute(WinTypes.HANDLE hwnd, int dwAttribute, PointerType pvAttribute, int cbAttribute);
}
@@ -96,4 +96,8 @@ public interface WinConstants {
int RelationNumaNodeEx = 6;
int RelationProcessorModule = 7;
int RelationAll = 0xffff;
// https://learn.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;
}
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.util.platform.windows;
import com.sun.jna.*;
import com.sun.jna.ptr.ByReference;
import com.sun.jna.ptr.LongByReference;
import java.util.Arrays;
@@ -27,6 +28,97 @@ import java.util.List;
* @author Glavo
*/
public interface WinTypes {
/// @see <a href="https://learn.microsoft.com/windows/win32/winprog/windows-data-types">Windows Data Types</a>
final class BOOL extends IntegerType {
public static final int SIZE = 4;
public BOOL() {
this(0);
}
public BOOL(boolean value) {
this(value ? 1L : 0L);
}
public BOOL(long value) {
super(SIZE, value, false);
assert value == 0 || value == 1;
}
public boolean booleanValue() {
return this.intValue() > 0;
}
@Override
public String toString() {
return Boolean.toString(booleanValue());
}
}
/// @see <a href="https://learn.microsoft.com/windows/win32/winprog/windows-data-types">Windows Data Types</a>
final class BOOLByReference extends ByReference {
public BOOLByReference() {
this(new BOOL(0));
}
public BOOLByReference(BOOL value) {
super(BOOL.SIZE);
setValue(value);
}
public void setValue(BOOL value) {
getPointer().setInt(0, value.intValue());
}
public BOOL getValue() {
return new BOOL(getPointer().getInt(0));
}
}
/// @see <a href="https://learn.microsoft.com/windows/win32/winprog/windows-data-types">Windows Data Types</a>
final class HANDLE extends PointerType {
public static final long INVALID_VALUE = Native.POINTER_SIZE == 8 ? -1 : 0xFFFFFFFFL;
public static final HANDLE INVALID = new HANDLE(Pointer.createConstant(INVALID_VALUE));
private boolean immutable;
public HANDLE() {
}
public HANDLE(Pointer p) {
setPointer(p);
immutable = true;
}
@Override
public Object fromNative(Object nativeValue, FromNativeContext context) {
Object o = super.fromNative(nativeValue, context);
if (INVALID.equals(o)) {
return INVALID;
}
return o;
}
@Override
public void setPointer(Pointer p) {
if (immutable) {
throw new UnsupportedOperationException("immutable reference");
}
super.setPointer(p);
}
@Override
public String toString() {
return String.valueOf(getPointer());
}
}
/**
* @see <a href="https://learn.microsoft.com/windows/win32/api/winnt/ns-winnt-osversioninfoexw">OSVERSIONINFOEXW structure</a>
*/
@@ -17,7 +17,7 @@
*/
package org.jackhuang.hmcl.util.versioning;
import org.intellij.lang.annotations.MagicConstant;
import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jetbrains.annotations.NotNull;
import java.io.BufferedReader;
@@ -28,6 +28,8 @@ import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
@@ -38,6 +40,10 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
}
public static GameVersionNumber asGameVersion(String version) {
GameVersionNumber versionNumber = Versions.SPECIALS.get(version);
if (versionNumber != null)
return versionNumber;
try {
if (!version.isEmpty()) {
char ch = version.charAt(0);
@@ -53,20 +59,15 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
if (version.equals("0.0"))
return Release.ZERO;
if (version.startsWith("1."))
return Release.parse(version);
if (version.length() >= 6 && version.charAt(2) == 'w')
return LegacySnapshot.parse(version);
if (version.length() == 6 && version.charAt(2) == 'w')
return Snapshot.parse(version);
return Release.parse(version);
}
} catch (IllegalArgumentException ignore) {
} catch (Throwable ignore) {
}
Special special = Versions.SPECIALS.get(version);
if (special == null) {
special = new Special(version);
}
return special;
return new Special(version, version);
}
public static GameVersionNumber asGameVersion(Optional<String> version) {
@@ -94,17 +95,22 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
}
final String value;
final String normalized;
GameVersionNumber(String value) {
GameVersionNumber(String value, String normalized) {
this.value = value;
this.normalized = normalized;
}
public boolean isAprilFools() {
if (this instanceof Special && !value.endsWith("_unobfuscated"))
return true;
if (this instanceof Special) {
String normalizedVersion = this.toNormalizedString();
return !normalizedVersion.startsWith("1.") && !normalizedVersion.equals("13w12~")
|| normalizedVersion.equals("1.RV-Pre1");
}
if (this instanceof Snapshot snapshot) {
return snapshot.intValue == Snapshot.toInt(15, 14, 'a');
if (this instanceof LegacySnapshot snapshot) {
return snapshot.intValue == LegacySnapshot.toInt(15, 14, 'a', false);
}
return false;
@@ -144,10 +150,10 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
///
/// ```java
/// GameVersionNumber.asVersion("...").isAtLeast("1.13", "17w43a");
/// ```
///```
///
/// @param strictReleaseVersion When `strictReleaseVersion` is `false`, `releaseVersion` is considered less than
/// its corresponding pre/rc versions.
/// its corresponding pre/rc versions.
public boolean isAtLeast(@NotNull String releaseVersion, @NotNull String snapshotVersion, boolean strictReleaseVersion) {
if (this instanceof Release self) {
Release other;
@@ -159,17 +165,35 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
return self.compareToRelease(other) >= 0;
} else {
return this.compareTo(Snapshot.parse(snapshotVersion)) >= 0;
return this.compareTo(LegacySnapshot.parse(snapshotVersion)) >= 0;
}
}
public String toNormalizedString() {
return normalized;
}
@Override
public String toString() {
return value;
}
protected ToStringBuilder buildDebugString() {
return new ToStringBuilder(this)
.append("value", value)
.append("normalized", normalized)
.append("type", getType());
}
public final String toDebugString() {
return buildDebugString().toString();
}
public static final class Old extends GameVersionNumber {
static Old parse(String value) {
if (value.isEmpty())
throw new IllegalArgumentException("Empty old version number");
Type type;
int prefixLength = 1;
switch (value.charAt(0)) {
@@ -215,7 +239,7 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
final VersionNumber versionNumber;
private Old(String value, Type type, VersionNumber versionNumber) {
super(value);
super(value, value);
this.type = type;
this.versionNumber = versionNumber;
}
@@ -231,83 +255,141 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
return o instanceof Old other && type == other.type && this.versionNumber.compareTo(other.versionNumber) == 0;
public int hashCode() {
return Objects.hash(type, versionNumber);
}
@Override
public int hashCode() {
return Objects.hash(type, versionNumber.hashCode());
public boolean equals(Object o) {
return o instanceof Old that
&& this.type == that.type
&& this.versionNumber.equals(that.versionNumber);
}
}
public static final class Release extends GameVersionNumber {
private static final int MINIMUM_YEAR_MAJOR_VERSION = 25;
private static final Pattern PATTERN = Pattern.compile("1\\.(?<minor>[0-9]+)(\\.(?<patch>[0-9]+))?((?<eaType>(-[a-zA-Z]+| Pre-Release ))(?<eaVersion>.+))?");
public enum ReleaseType {
UNKNOWN(""),
SNAPSHOT("-snapshot-"),
PRE_RELEASE("-pre"),
RELEASE_CANDIDATE("-rc"),
GA("");
private final String infix;
public static final int TYPE_GA = Integer.MAX_VALUE;
ReleaseType(String infix) {
this.infix = infix;
}
}
public static final int TYPE_UNKNOWN = 0;
public static final int TYPE_EXP = 1;
public static final int TYPE_PRE = 2;
public static final int TYPE_RC = 3;
public enum Additional {
NONE(""), UNOBFUSCATED("_unobfuscated");
private final String suffix;
static final Release ZERO = new Release("0.0", 0, 0, 0, TYPE_GA, VersionNumber.ZERO);
Additional(String suffix) {
this.suffix = suffix;
}
}
static final Release ZERO = new Release(
"0.0", "0.0",
0, 0, 0,
ReleaseType.UNKNOWN, VersionNumber.ZERO, Additional.NONE
);
private static final Pattern VERSION_PATTERN = Pattern.compile("(?<prefix>(?<major>1|[1-9]\\d+)\\.(?<minor>\\d+)(\\.(?<patch>[0-9]+))?)(?<suffix>.*)");
static Release parse(String value) {
Matcher matcher = PATTERN.matcher(value);
Matcher matcher = VERSION_PATTERN.matcher(value);
if (!matcher.matches()) {
throw new IllegalArgumentException(value);
}
int major = Integer.parseInt(matcher.group("major"));
if (major != 1 && major < MINIMUM_YEAR_MAJOR_VERSION)
throw new IllegalArgumentException(value);
int minor = Integer.parseInt(matcher.group("minor"));
String patchString = matcher.group("patch");
int patch = patchString != null ? Integer.parseInt(patchString) : 0;
String eaTypeString = matcher.group("eaType");
int eaType;
if (eaTypeString == null) {
eaType = TYPE_GA;
} else if ("-pre".equals(eaTypeString) || " Pre-Release ".equals(eaTypeString)) {
eaType = TYPE_PRE;
} else if ("-rc".equals(eaTypeString)) {
eaType = TYPE_RC;
} else if ("-exp".equals(eaTypeString)) {
eaType = TYPE_EXP;
String suffix = matcher.group("suffix");
ReleaseType releaseType;
VersionNumber eaVersion;
Additional additional = Additional.NONE;
boolean needNormalize = false;
if (suffix.endsWith("_unobfuscated")) {
suffix = suffix.substring(0, suffix.length() - "_unobfuscated".length());
additional = Additional.UNOBFUSCATED;
} else if (suffix.endsWith(" Unobfuscated")) {
needNormalize = true;
suffix = suffix.substring(0, suffix.length() - " Unobfuscated".length());
additional = Additional.UNOBFUSCATED;
}
if (suffix.isEmpty()) {
releaseType = ReleaseType.GA;
eaVersion = VersionNumber.ZERO;
} else if (suffix.startsWith("-snapshot-")) {
releaseType = ReleaseType.SNAPSHOT;
eaVersion = VersionNumber.asVersion(suffix.substring("-snapshot-".length()));
} else if (suffix.startsWith(" Snapshot ")) {
needNormalize = true;
releaseType = ReleaseType.SNAPSHOT;
eaVersion = VersionNumber.asVersion(suffix.substring(" Snapshot ".length()));
} else if (suffix.startsWith("-pre")) {
releaseType = ReleaseType.PRE_RELEASE;
eaVersion = VersionNumber.asVersion(suffix.substring("-pre".length()));
} else if (suffix.startsWith(" Pre-Release ")) {
needNormalize = true;
releaseType = ReleaseType.PRE_RELEASE;
eaVersion = VersionNumber.asVersion(suffix.substring(" Pre-Release ".length()));
} else if (suffix.startsWith("-rc")) {
releaseType = ReleaseType.RELEASE_CANDIDATE;
eaVersion = VersionNumber.asVersion(suffix.substring("-rc".length()));
} else if (suffix.startsWith(" Release Candidate ")) {
needNormalize = true;
releaseType = ReleaseType.RELEASE_CANDIDATE;
eaVersion = VersionNumber.asVersion(suffix.substring(" Release Candidate ".length()));
} else {
eaType = TYPE_UNKNOWN;
throw new IllegalArgumentException(value);
}
String eaVersionString = matcher.group("eaVersion");
VersionNumber eaVersion = eaVersionString != null ? VersionNumber.asVersion(eaVersionString) : VersionNumber.ZERO;
return new Release(value, 1, minor, patch, eaType, eaVersion);
}
private static int getNumberLength(String value, int offset) {
int current = offset;
while (current < value.length()) {
char ch = value.charAt(current);
if (ch < '0' || ch > '9')
break;
current++;
String normalized;
if (needNormalize) {
StringBuilder builder = new StringBuilder(value.length());
builder.append(matcher.group("prefix"));
if (releaseType != ReleaseType.GA) {
builder.append(releaseType.infix);
builder.append(eaVersion);
}
builder.append(additional.suffix);
normalized = builder.toString();
} else {
normalized = value;
}
return current - offset;
return new Release(value, normalized, major, minor, patch, releaseType, eaVersion, additional);
}
/// Quickly parses a simple format (`1\.[0-9]+(\.[0-9]+)?`) release version.
/// The returned [#eaType] will be set to [#TYPE_UNKNOWN], meaning it will be less than all pre/rc and official versions of this version.
/// Quickly parses a simple format (`[1-9][0-9]+\.[0-9]+(\.[0-9]+)?`) release version.
/// The returned [#eaType] will be set to [ReleaseType#UNKNOWN], meaning it will be less than all pre/rc and official versions of this version.
///
/// @see GameVersionNumber#isAtLeast(String, String)
static Release parseSimple(String value) {
if (!value.startsWith("1."))
int majorLength = getNumberLength(value, 0);
if (majorLength == 0 || value.length() < majorLength + 2 || value.charAt(majorLength) != '.')
throw new IllegalArgumentException(value);
final int minorOffset = 2;
int major = Integer.parseInt(value.substring(0, majorLength));
if (major != 1 && major < MINIMUM_YEAR_MAJOR_VERSION)
throw new IllegalArgumentException(value);
final int minorOffset = majorLength + 1;
int minorLength = getNumberLength(value, minorOffset);
if (minorLength == 0)
@@ -326,27 +408,41 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
patch = Integer.parseInt(value.substring(patchOffset));
}
return new Release(value, 1, minor, patch, TYPE_UNKNOWN, VersionNumber.ZERO);
return new Release(value, value, major, minor, patch, ReleaseType.UNKNOWN, VersionNumber.ZERO, Additional.NONE);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(value);
}
}
private static int getNumberLength(String value, int offset) {
int current = offset;
while (current < value.length()) {
char ch = value.charAt(current);
if (ch < '0' || ch > '9')
break;
current++;
}
return current - offset;
}
private final int major;
private final int minor;
private final int patch;
@MagicConstant(intValues = {TYPE_GA, TYPE_UNKNOWN, TYPE_EXP, TYPE_PRE, TYPE_RC})
private final int eaType;
private final ReleaseType eaType;
private final VersionNumber eaVersion;
private final Additional additional;
Release(String value, int major, int minor, int patch, int eaType, VersionNumber eaVersion) {
super(value);
Release(String value, String normalized, int major, int minor, int patch, ReleaseType eaType, VersionNumber eaVersion, Additional additional) {
super(value, normalized);
this.major = major;
this.minor = minor;
this.patch = patch;
this.eaType = eaType;
this.eaVersion = eaVersion;
this.additional = additional;
}
@Override
@@ -367,35 +463,45 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
if (c != 0)
return c;
c = Integer.compare(this.eaType, other.eaType);
c = this.eaType.compareTo(other.eaType);
if (c != 0)
return c;
return this.eaVersion.compareTo(other.eaVersion);
c = this.eaVersion.compareTo(other.eaVersion);
if (c != 0)
return c;
return this.additional.compareTo(other.additional);
}
int compareToSnapshot(Snapshot other) {
int idx = Arrays.binarySearch(Versions.SNAPSHOT_INTS, other.intValue);
if (idx >= 0)
return this.compareToRelease(Versions.SNAPSHOT_PREV[idx]) <= 0 ? -1 : 1;
idx = -(idx + 1);
if (idx == Versions.SNAPSHOT_INTS.length)
int compareToSnapshot(LegacySnapshot other) {
if (major == 0) {
return -1;
} else if (major == 1) {
int idx = Arrays.binarySearch(Versions.SNAPSHOT_INTS, other.intValue);
if (idx >= 0)
return this.compareToRelease(Versions.SNAPSHOT_PREV[idx]) <= 0 ? -1 : 1;
return this.compareToRelease(Versions.SNAPSHOT_PREV[idx]) <= 0 ? -1 : 1;
idx = -(idx + 1);
if (idx == Versions.SNAPSHOT_INTS.length)
return -1;
return this.compareToRelease(Versions.SNAPSHOT_PREV[idx]) <= 0 ? -1 : 1;
} else {
return 1;
}
}
@Override
int compareToImpl(@NotNull GameVersionNumber other) {
if (other instanceof Release)
return compareToRelease((Release) other);
if (other instanceof Release release)
return compareToRelease(release);
if (other instanceof Snapshot)
return compareToSnapshot((Snapshot) other);
if (other instanceof LegacySnapshot snapshot)
return compareToSnapshot(snapshot);
if (other instanceof Special)
return -((Special) other).compareToReleaseOrSnapshot(this);
if (other instanceof Special special)
return -special.compareToReleaseOrSnapshot(this);
throw new AssertionError(other.getClass());
}
@@ -412,7 +518,7 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
return patch;
}
public int getEaType() {
public ReleaseType getEaType() {
return eaType;
}
@@ -420,28 +526,65 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
return eaVersion;
}
public Additional getAdditional() {
return additional;
}
@Override
public int hashCode() {
return Objects.hash(major, minor, patch, eaType, eaVersion);
return Objects.hash(major, minor, patch, eaType, eaVersion, additional);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
return o instanceof Release other
&& major == other.major
&& minor == other.minor
&& patch == other.patch
&& eaType == other.eaType
&& eaVersion.equals(other.eaVersion);
return o instanceof Release that
&& this.major == that.major
&& this.minor == that.minor
&& this.patch == that.patch
&& this.eaType == that.eaType
&& this.eaVersion.equals(that.eaVersion)
&& this.additional == that.additional;
}
@Override
protected ToStringBuilder buildDebugString() {
return super.buildDebugString()
.append("major", major)
.append("minor", minor)
.append("patch", patch)
.append("eaType", eaType)
.append("eaVersion", eaVersion)
.append("additional", additional);
}
}
public static final class Snapshot extends GameVersionNumber {
static Snapshot parse(String value) {
if (value.length() != 6 || value.charAt(2) != 'w')
/// Legacy snapshot version numbers like `25w46a`.
public static final class LegacySnapshot extends GameVersionNumber {
static LegacySnapshot parse(String value) {
if (value.length() < 6 || value.charAt(2) != 'w')
throw new IllegalArgumentException(value);
int prefixLength;
boolean unobfuscated;
String normalized;
if (value.endsWith("_unobfuscated")) {
prefixLength = value.length() - "_unobfuscated".length();
unobfuscated = true;
normalized = value;
} else if (value.endsWith(" Unobfuscated")) {
prefixLength = value.length() - " Unobfuscated".length();
unobfuscated = true;
normalized = value.substring(0, prefixLength) + "_unobfuscated";
} else {
prefixLength = value.length();
unobfuscated = false;
normalized = value;
}
if (prefixLength != 6) {
throw new IllegalArgumentException(value);
}
int year;
int week;
try {
@@ -452,21 +595,21 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
}
char suffix = value.charAt(5);
if ((suffix < 'a' || suffix > 'z') && suffix != '~')
if (suffix < 'a' || suffix > 'z')
throw new IllegalArgumentException(value);
return new Snapshot(value, year, week, suffix);
return new LegacySnapshot(value, normalized, year, week, suffix, unobfuscated);
}
static int toInt(int year, int week, char suffix) {
return (year << 16) | (week << 8) | suffix;
static int toInt(int year, int week, char suffix, boolean unobfuscated) {
return (year << 24) | (week << 16) | (suffix << 8) | (unobfuscated ? 1 : 0);
}
final int intValue;
Snapshot(String value, int year, int week, char suffix) {
super(value);
this.intValue = toInt(year, week, suffix);
LegacySnapshot(String value, String normalized, int year, int week, char suffix, boolean unobfuscated) {
super(value, normalized);
this.intValue = toInt(year, week, suffix, unobfuscated);
}
@Override
@@ -476,40 +619,52 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
@Override
int compareToImpl(@NotNull GameVersionNumber other) {
if (other instanceof Release)
return -((Release) other).compareToSnapshot(this);
if (other instanceof Release otherRelease)
return -otherRelease.compareToSnapshot(this);
if (other instanceof Snapshot)
return Integer.compare(this.intValue, ((Snapshot) other).intValue);
if (other instanceof LegacySnapshot otherSnapshot)
return Integer.compare(this.intValue, otherSnapshot.intValue);
if (other instanceof Special)
return -((Special) other).compareToReleaseOrSnapshot(this);
if (other instanceof Special otherSpecial)
return -otherSpecial.compareToReleaseOrSnapshot(this);
throw new AssertionError(other.getClass());
}
public int getYear() {
return (intValue >> 16) & 0xff;
return (intValue >> 24) & 0xff;
}
public int getWeek() {
return (intValue >> 8) & 0xff;
return (intValue >> 16) & 0xff;
}
public char getSuffix() {
return (char) (intValue & 0xff);
return (char) ((intValue >> 8) & 0xff);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
return o instanceof Snapshot other && this.intValue == other.intValue;
public boolean isUnobfuscated() {
return (intValue & 0b00000001) != 0;
}
@Override
public int hashCode() {
return intValue;
}
@Override
public boolean equals(Object o) {
return o instanceof LegacySnapshot that && this.intValue == that.intValue;
}
@Override
protected ToStringBuilder buildDebugString() {
return super.buildDebugString()
.append("year", getYear())
.append("week", getWeek())
.append("suffix", getSuffix())
.append("unobfuscated", isUnobfuscated());
}
}
public static final class Special extends GameVersionNumber {
@@ -517,8 +672,8 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
private GameVersionNumber prev;
Special(String value) {
super(value);
Special(String value, String normalized) {
super(value, normalized);
}
@Override
@@ -534,13 +689,13 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
if (versionNumber != null)
return versionNumber;
return versionNumber = VersionNumber.asVersion(value);
return versionNumber = VersionNumber.asVersion(normalized);
}
GameVersionNumber getPrevNormalVersion() {
GameVersionNumber v = prev;
while (v instanceof Special) {
v = ((Special) v).prev;
while (v instanceof Special special) {
v = special.prev;
}
if (v == null) throw new AssertionError("version: " + value);
@@ -567,7 +722,7 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
if (other.isUnknown())
return -1;
if (this.value.equals(other.value))
if (this.normalized.equals(other.normalized))
return 0;
int c = this.getPrevNormalVersion().compareTo(other.getPrevNormalVersion());
@@ -575,11 +730,11 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
return c;
GameVersionNumber v = prev;
while (v instanceof Special) {
while (v instanceof Special special) {
if (v == other)
return 1;
v = ((Special) v).prev;
v = special.prev;
}
return -1;
@@ -587,27 +742,23 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
@Override
int compareToImpl(@NotNull GameVersionNumber o) {
if (o instanceof Release)
if (o instanceof Release || o instanceof LegacySnapshot)
return compareToReleaseOrSnapshot(o);
if (o instanceof Snapshot)
return compareToReleaseOrSnapshot(o);
if (o instanceof Special)
return compareToSpecial((Special) o);
if (o instanceof Special special)
return compareToSpecial(special);
throw new AssertionError(o.getClass());
}
@Override
public int hashCode() {
return value.hashCode();
public boolean equals(Object o) {
return o instanceof Special that && this.normalized.equals(that.normalized);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
return o instanceof Special other && this.value.equals(other.value);
public int hashCode() {
return normalized.hashCode();
}
}
@@ -621,10 +772,11 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
static {
ArrayDeque<String> defaultGameVersions = new ArrayDeque<>(64);
List<Snapshot> snapshots = new ArrayList<>(1024);
List<LegacySnapshot> snapshots = new ArrayList<>(1024);
List<Release> snapshotPrev = new ArrayList<>(1024);
try (BufferedReader reader = new BufferedReader(new InputStreamReader(GameVersionNumber.class.getResourceAsStream("/assets/game/versions.txt"), StandardCharsets.US_ASCII))) {
//noinspection DataFlowIssue
try (var reader = new BufferedReader(new InputStreamReader(GameVersionNumber.class.getResourceAsStream("/assets/game/versions.txt"), StandardCharsets.US_ASCII))) {
Release currentRelease = null;
GameVersionNumber prev = null;
@@ -637,13 +789,13 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
if (currentRelease == null)
currentRelease = (Release) version;
if (version instanceof Snapshot snapshot) {
if (version instanceof LegacySnapshot snapshot) {
snapshots.add(snapshot);
snapshotPrev.add(currentRelease);
} else if (version instanceof Release) {
currentRelease = (Release) version;
} else if (version instanceof Release release) {
currentRelease = release;
if (currentRelease.eaType == Release.TYPE_GA) {
if (currentRelease.eaType == Release.ReleaseType.GA) {
defaultGameVersions.addFirst(currentRelease.value);
}
} else if (version instanceof Special special) {
@@ -658,6 +810,36 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
throw new AssertionError(e);
}
//noinspection DataFlowIssue
try (var reader = new BufferedReader(new InputStreamReader(GameVersionNumber.class.getResourceAsStream("/assets/game/version-alias.csv"), StandardCharsets.US_ASCII))) {
for (String line; (line = reader.readLine()) != null; ) {
if (line.isEmpty())
continue;
String[] parts = line.split(",");
if (parts.length < 2) {
LOG.warning("Invalid line: " + line);
continue;
}
String normalized = parts[0];
Special normalizedVersion = SPECIALS.get(normalized);
if (normalizedVersion == null) {
LOG.warning("Unknown special version: " + normalized);
continue;
}
for (int i = 1; i < parts.length; i++) {
String version = parts[i];
Special versionNumber = new Special(version, normalized);
versionNumber.prev = normalizedVersion.prev;
SPECIALS.put(version, versionNumber);
}
}
} catch (IOException e) {
throw new AssertionError(e);
}
DEFAULT_GAME_VERSIONS = defaultGameVersions.toArray(new String[0]);
SNAPSHOT_INTS = new int[snapshots.size()];
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="warn" packages="">
<Appenders>
<Console name="SysOut" target="SYSTEM_OUT">
<PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg{nolookups}%n" />
</Console>
<Queue name="ServerGuiConsole">
<PatternLayout pattern="[%d{HH:mm:ss} %level]: %msg{nolookups}%n" />
</Queue>
<RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level] [%logger]: %msg{nolookups}%n"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<OnStartupTriggeringPolicy/>
</Policies>
</RollingRandomAccessFile>
<RollingRandomAccessFile name="DebugFile" fileName="logs/debug.log" filePattern="logs/debug-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss}] [%t/%level] [%logger]: %msg{nolookups}%n"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<OnStartupTriggeringPolicy/>
</Policies>
<DefaultRolloverStrategy max="999" fileIndex="min"/>
</RollingRandomAccessFile>
</Appenders>
<Loggers>
<Root level="all">
<filters>
<MarkerFilter marker="NETWORK_PACKETS" onMatch="DENY" onMismatch="NEUTRAL"/>
</filters>
<AppenderRef ref="SysOut" level="info"/>
<AppenderRef ref="ServerGuiConsole" level="info"/>
<AppenderRef ref="File" level="info"/>
<AppenderRef ref="DebugFile" level="all"/>
</Root>
</Loggers>
</Configuration>
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="SysOut" target="SYSTEM_OUT">
<PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg%n" />
</Console>
<Queue name="ServerGuiConsole">
<PatternLayout pattern="[%d{HH:mm:ss} %level]: %msg%n" />
</Queue>
<RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level] [%logger]: %msg%n"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<OnStartupTriggeringPolicy/>
</Policies>
</RollingRandomAccessFile>
<RollingRandomAccessFile name="DebugFile" fileName="logs/debug.log" filePattern="logs/debug-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss}] [%t/%level] [%logger]: %msg%n"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<OnStartupTriggeringPolicy/>
</Policies>
</RollingRandomAccessFile>
</Appenders>
<Loggers>
<Root level="all">
<filters>
<MarkerFilter marker="NETWORK_PACKETS" onMatch="DENY" onMismatch="NEUTRAL"/>
</filters>
<AppenderRef ref="SysOut" level="info"/>
<AppenderRef ref="ServerGuiConsole" level="info"/>
<AppenderRef ref="File" level="info"/>
<AppenderRef ref="DebugFile" level="all"/>
</Root>
</Loggers>
</Configuration>
@@ -1,5 +1,40 @@
{
"versions": [
{
"id": "1.21.11_unobfuscated",
"type": "unobfuscated",
"url": "https://piston-meta.mojang.com/v1/packages/327be7759157b04495c591dbb721875e341877af/1.21.11_unobfuscated.json",
"time": "2025-12-09T12:43:15+00:00",
"releaseTime": "2025-12-09T12:43:15+00:00"
},
{
"id": "1.21.11-rc2_unobfuscated",
"type": "unobfuscated",
"url": "https://piston-meta.mojang.com/v1/packages/9282a3fb154d2a425086c62c11827281308bf93b/1.21.11-rc2_unobfuscated.json",
"time": "2025-12-05T11:57:45+00:00",
"releaseTime": "2025-12-05T11:57:45+00:00"
},
{
"id": "1.21.11-rc1_unobfuscated",
"type": "unobfuscated",
"url": "https://piston-meta.mojang.com/v1/packages/5d3ee0ef1f0251cf7e073354ca9e085a884a643d/1.21.11-rc1_unobfuscated.json",
"time": "2025-12-04T15:56:55+00:00",
"releaseTime": "2025-12-04T15:56:55+00:00"
},
{
"id": "1.21.11-pre5_unobfuscated",
"type": "unobfuscated",
"url": "https://piston-meta.mojang.com/v1/packages/1028441ca6d288bbf2103e773196bf524f7260fd/1.21.11-pre5_unobfuscated.json",
"time": "2025-12-03T13:34:06+00:00",
"releaseTime": "2025-12-03T13:34:06+00:00"
},
{
"id": "1.21.11-pre4_unobfuscated",
"type": "unobfuscated",
"url": "https://piston-meta.mojang.com/v1/packages/410ce37a2506adcfd54ef7d89168cfbe89cac4cb/1.21.11-pre4_unobfuscated.json",
"time": "2025-12-01T13:40:12+00:00",
"releaseTime": "2025-12-01T13:40:12+00:00"
},
{
"id": "1.21.11-pre3_unobfuscated",
"type": "unobfuscated",
@@ -92,63 +127,63 @@
"releaseTime": "2021-07-13T12:54:19+00:00"
},
{
"id": "1_16_combat-6",
"id": "1.16_combat-6",
"type": "pending",
"url": "https://zkitefly.github.io/unlisted-versions-of-minecraft/files/1_16_combat-6/1_16_combat-6.json",
"time": "2020-08-26T06:24:28+00:00",
"releaseTime": "2020-08-26T06:24:28+00:00"
},
{
"id": "1_16_combat-5",
"id": "1.16_combat-5",
"type": "pending",
"url": "https://zkitefly.github.io/unlisted-versions-of-minecraft/files/1_16_combat-5/1_16_combat-5.json",
"time": "2020-08-21T09:23:13+00:00",
"releaseTime": "2020-08-21T09:23:13+00:00"
},
{
"id": "1_16_combat-4",
"id": "1.16_combat-4",
"type": "pending",
"url": "https://zkitefly.github.io/unlisted-versions-of-minecraft/files/1_16_combat-4/1_16_combat-4.json",
"time": "2020-08-19T11:14:58+00:00",
"releaseTime": "2020-08-19T11:14:58+00:00"
},
{
"id": "1_16_combat-3",
"id": "1.16_combat-3",
"type": "pending",
"url": "https://zkitefly.github.io/unlisted-versions-of-minecraft/files/1_16_combat-3/1_16_combat-3.json",
"time": "2020-08-14T09:02:15+00:00",
"releaseTime": "2020-08-14T09:02:15+00:00"
},
{
"id": "1_16_combat-2",
"id": "1.16_combat-2",
"type": "pending",
"url": "https://zkitefly.github.io/unlisted-versions-of-minecraft/files/1_16_combat-2/1_16_combat-2.json",
"time": "2020-08-13T11:56:30+00:00",
"releaseTime": "2020-08-13T11:56:30+00:00"
},
{
"id": "1_16_combat-1",
"id": "1.16_combat-1",
"type": "pending",
"url": "https://zkitefly.github.io/unlisted-versions-of-minecraft/files/1_16_combat-1/1_16_combat-1.json",
"time": "2020-08-12T14:07:25+00:00",
"releaseTime": "2020-08-12T14:07:25+00:00"
},
{
"id": "1_16_combat-0",
"id": "1.16_combat-0",
"type": "pending",
"url": "https://zkitefly.github.io/unlisted-versions-of-minecraft/files/1_16_combat-0/1_16_combat-0.json",
"time": "2020-08-07T10:44:47+00:00",
"releaseTime": "2020-08-07T10:44:47+00:00"
},
{
"id": "1_15_combat-6",
"id": "1.15_combat-6",
"type": "pending",
"url": "https://zkitefly.github.io/unlisted-versions-of-minecraft/files/1_15_combat-6/1_15_combat-6.json",
"time": "2020-01-15T09:46:35+00:00",
"releaseTime": "2020-01-15T09:46:35+00:00"
},
{
"id": "1_15_combat-1",
"id": "1.15_combat-1",
"type": "pending",
"url": "https://zkitefly.github.io/unlisted-versions-of-minecraft/files/1_15_combat-1/1_15_combat-1.json",
"time": "2019-11-29T15:41:39+00:00",
@@ -0,0 +1,20 @@
1.14_combat-212796,1.14.3 - Combat Test
1.14_combat-0,Combat Test 2
1.14_combat-3,Combat Test 3
1.15_combat-1,Combat Test 4
1.15_combat-6,Combat Test 5
1.16_combat-0,Combat Test 6
1.16_combat-1,Combat Test 7
1.16_combat-2,Combat Test 7b
1.16_combat-3,Combat Test 7c
1.16_combat-4,Combat Test 8
1.16_combat-5,Combat Test 8b
1.16_combat-6,Combat Test 8c
1.18_experimental-snapshot-1,1.18 Experimental Snapshot 1
1.18_experimental-snapshot-2,1.18 Experimental Snapshot 2
1.18_experimental-snapshot-3,1.18 Experimental Snapshot 3
1.18_experimental-snapshot-4,1.18 Experimental Snapshot 4
1.18_experimental-snapshot-5,1.18 Experimental Snapshot 5
1.18_experimental-snapshot-6,1.18 Experimental Snapshot 6
1.19_deep_dark_experimental_snapshot-1,Deep Dark Experimental Snapshot 1
20w14infinite,20w14~
1 1.14_combat-212796 1.14.3 - Combat Test
2 1.14_combat-0 Combat Test 2
3 1.14_combat-3 Combat Test 3
4 1.15_combat-1 Combat Test 4
5 1.15_combat-6 Combat Test 5
6 1.16_combat-0 Combat Test 6
7 1.16_combat-1 Combat Test 7
8 1.16_combat-2 Combat Test 7b
9 1.16_combat-3 Combat Test 7c
10 1.16_combat-4 Combat Test 8
11 1.16_combat-5 Combat Test 8b
12 1.16_combat-6 Combat Test 8c
13 1.18_experimental-snapshot-1 1.18 Experimental Snapshot 1
14 1.18_experimental-snapshot-2 1.18 Experimental Snapshot 2
15 1.18_experimental-snapshot-3 1.18 Experimental Snapshot 3
16 1.18_experimental-snapshot-4 1.18 Experimental Snapshot 4
17 1.18_experimental-snapshot-5 1.18 Experimental Snapshot 5
18 1.18_experimental-snapshot-6 1.18 Experimental Snapshot 6
19 1.19_deep_dark_experimental_snapshot-1 Deep Dark Experimental Snapshot 1
20 20w14infinite 20w14~
@@ -452,24 +452,25 @@
3D Shareware v1.34
19w14a
19w14b
1.14 Pre-Release 1
1.14 Pre-Release 2
1.14 Pre-Release 3
1.14 Pre-Release 4
1.14 Pre-Release 5
1.14-pre1
1.14-pre2
1.14-pre3
1.14-pre4
1.14-pre5
1.14
1.14.1 Pre-Release 1
1.14.1 Pre-Release 2
1.14.1-pre1
1.14.1-pre2
1.14.1
1.14.2 Pre-Release 1
1.14.2 Pre-Release 2
1.14.2 Pre-Release 3
1.14.2 Pre-Release 4
1.14.2-pre1
1.14.2-pre2
1.14.2-pre3
1.14.2-pre4
1.14.2
1.14.3-pre1
1.14.3-pre2
1.14.3-pre3
1.14.3-pre4
1.14_combat-212796
1.14.3
1.14.4-pre1
1.14.4-pre2
@@ -479,6 +480,8 @@
1.14.4-pre6
1.14.4-pre7
1.14.4
1.14_combat-0
1.14_combat-3
19w34a
19w35a
19w36a
@@ -497,6 +500,7 @@
1.15-pre1
1.15-pre2
1.15-pre3
1.15_combat-1
1.15-pre4
1.15-pre5
1.15-pre6
@@ -506,6 +510,7 @@
1.15.1
1.15.2-pre1
1.15.2-pre2
1.15_combat-6
1.15.2
20w06a
20w07a
@@ -545,9 +550,16 @@
1.16.2-pre1
1.16.2-pre2
1.16.2-pre3
1.16_combat-0
1.16.2-rc1
1.16.2-rc2
1.16.2
1.16_combat-1
1.16_combat-2
1.16_combat-3
1.16_combat-4
1.16_combat-5
1.16_combat-6
1.16.3-rc1
1.16.3
1.16.4-pre1
@@ -592,6 +604,13 @@
1.17.1-rc1
1.17.1-rc2
1.17.1
1.18_experimental-snapshot-1
1.18_experimental-snapshot-2
1.18_experimental-snapshot-3
1.18_experimental-snapshot-4
1.18_experimental-snapshot-5
1.18_experimental-snapshot-6
1.18_experimental-snapshot-7
21w37a
21w38a
21w39a
@@ -622,6 +641,7 @@
22w05a
22w06a
22w07a
1.19_deep_dark_experimental_snapshot-1
1.18.2-pre1
1.18.2-pre2
1.18.2-pre3
@@ -862,4 +882,14 @@
1.21.11-pre2
1.21.11-pre2_unobfuscated
1.21.11-pre3
1.21.11-pre3_unobfuscated
1.21.11-pre3_unobfuscated
1.21.11-pre4
1.21.11-pre4_unobfuscated
1.21.11-pre5
1.21.11-pre5_unobfuscated
1.21.11-rc1
1.21.11-rc1_unobfuscated
1.21.11-rc2
1.21.11-rc2_unobfuscated
1.21.11
1.21.11_unobfuscated
@@ -25,6 +25,7 @@ import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.Supplier;
import static org.jackhuang.hmcl.util.versioning.GameVersionNumber.asGameVersion;
import static org.junit.jupiter.api.Assertions.*;
@@ -34,6 +35,8 @@ import static org.junit.jupiter.api.Assertions.*;
*/
public final class GameVersionNumberTest {
//region Helpers
private static List<String> readVersions() {
List<String> versions = new ArrayList<>();
@@ -48,18 +51,8 @@ public final class GameVersionNumberTest {
return versions;
}
@Test
public void testSortVersions() {
List<String> versions = readVersions();
List<String> copied = new ArrayList<>(versions);
Collections.shuffle(copied, new Random(0));
copied.sort(Comparator.comparing(GameVersionNumber::asGameVersion));
assertIterableEquals(versions, copied);
}
private static String errorMessage(String version1, String version2) {
return String.format("version1=%s, version2=%s", version1, version2);
private static Supplier<String> errorMessage(GameVersionNumber version1, GameVersionNumber version2) {
return () -> "version1=%s, version2=%s".formatted(version1.toDebugString(), version2.toDebugString());
}
private static void assertGameVersionEquals(String version) {
@@ -67,25 +60,42 @@ public final class GameVersionNumberTest {
}
private static void assertGameVersionEquals(String version1, String version2) {
assertEquals(0, asGameVersion(version1).compareTo(version2), errorMessage(version1, version2));
assertEquals(asGameVersion(version1), asGameVersion(version2), errorMessage(version1, version2));
}
private static String toString(GameVersionNumber gameVersionNumber) {
return gameVersionNumber.getClass().getSimpleName();
GameVersionNumber gameVersion1 = asGameVersion(version1);
GameVersionNumber gameVersion2 = asGameVersion(version2);
assertEquals(0, gameVersion1.compareTo(gameVersion2), errorMessage(gameVersion1, gameVersion2));
assertEquals(0, gameVersion2.compareTo(gameVersion1), errorMessage(gameVersion1, gameVersion2));
assertEquals(gameVersion1, gameVersion2, errorMessage(gameVersion1, gameVersion2));
assertEquals(gameVersion2, gameVersion1, errorMessage(gameVersion1, gameVersion2));
assertEquals(gameVersion1.hashCode(), gameVersion2.hashCode(), errorMessage(gameVersion1, gameVersion2));
}
private static void assertOrder(String... versions) {
var gameVersionNumbers = new GameVersionNumber[versions.length];
for (int i = 0; i < versions.length; i++) {
gameVersionNumbers[i] = asGameVersion(versions[i]);
}
for (int i = 0; i < versions.length - 1; i++) {
GameVersionNumber version1 = asGameVersion(versions[i]);
GameVersionNumber version1 = gameVersionNumbers[i];
for (int j = 0; j < i; j++) {
GameVersionNumber version2 = gameVersionNumbers[j];
assertTrue(version1.compareTo(version2) > 0, errorMessage(version1, version2));
assertTrue(version2.compareTo(version1) < 0, errorMessage(version1, version2));
assertNotEquals(version1, version2, errorMessage(version1, version2));
assertNotEquals(version2, version1, errorMessage(version1, version2));
}
assertGameVersionEquals(versions[i]);
for (int j = i + 1; j < versions.length; j++) {
GameVersionNumber version2 = asGameVersion(versions[j]);
GameVersionNumber version2 = gameVersionNumbers[j];
assertEquals(-1, version1.compareTo(version2), String.format("version1=%s (%s), version2=%s (%s)", versions[i], toString(version1), versions[j], toString(version2)));
assertEquals(1, version2.compareTo(version1), String.format("version1=%s (%s), version2=%s (%s)", versions[i], toString(version1), versions[j], toString(version2)));
assertTrue(version1.compareTo(version2) < 0, errorMessage(version1, version2));
assertTrue(version2.compareTo(version1) > 0, errorMessage(version1, version2));
assertNotEquals(version1, version2, errorMessage(version1, version2));
assertNotEquals(version2, version1, errorMessage(version1, version2));
}
}
@@ -100,6 +110,8 @@ public final class GameVersionNumberTest {
assertEquals(VersionNumber.asVersion(versionNumber), old.versionNumber);
}
//endregion Helpers
private static boolean isAprilFools(String version) {
return asGameVersion(version).isAprilFools();
}
@@ -124,6 +136,31 @@ public final class GameVersionNumberTest {
assertFalse(isAprilFools("25w45a_unobfuscated"));
}
@Test
public void testSortVersions() {
List<String> versions = readVersions();
{
List<String> copied = new ArrayList<>(versions);
copied.sort(Comparator.comparing(GameVersionNumber::asGameVersion));
assertIterableEquals(versions, copied);
}
{
List<String> copied = new ArrayList<>(versions);
Collections.reverse(copied);
copied.sort(Comparator.comparing(GameVersionNumber::asGameVersion));
assertIterableEquals(versions, copied);
}
for (int randomSeed = 0; randomSeed < 5; randomSeed++) {
List<String> copied = new ArrayList<>(versions);
Collections.shuffle(copied, new Random(randomSeed));
copied.sort(Comparator.comparing(GameVersionNumber::asGameVersion));
assertIterableEquals(versions, copied);
}
}
@Test
public void testParseOld() {
assertOldVersion("rd-132211", GameVersionNumber.Type.PRE_CLASSIC, "132211");
@@ -136,36 +173,96 @@ public final class GameVersionNumberTest {
assertOldVersion("a1.0.13_01-1", GameVersionNumber.Type.ALPHA, "1.0.13_01-1");
assertOldVersion("b1.0", GameVersionNumber.Type.BETA, "1.0");
assertOldVersion("b1.0_01", GameVersionNumber.Type.BETA, "1.0_01");
assertOldVersion("b1.6-tb3", GameVersionNumber.Type.BETA, "1.6-tb3");
assertOldVersion("b1.8-pre1-2", GameVersionNumber.Type.BETA, "1.8-pre1-2");
assertOldVersion("b1.9-pre1", GameVersionNumber.Type.BETA, "1.9-pre1");
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Old.parse(""));
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Old.parse("1.21"));
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Old.parse("r-132211"));
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Old.parse("rd-"));
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Old.parse("rd-a"));
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Old.parse("i-20100223"));
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Old.parse("in-"));
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Old.parse("in-a"));
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Old.parse("inf-"));
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Old.parse("inf-a"));
}
private static void testParseLegacySnapshot(int year, int week, char suffix) {
String raw = "%02dw%02d%s".formatted(year, week, suffix);
var rawVersion = (GameVersionNumber.LegacySnapshot) asGameVersion(raw);
assertInstanceOf(GameVersionNumber.LegacySnapshot.class, rawVersion);
assertEquals(raw, rawVersion.toString());
assertEquals(raw, rawVersion.toNormalizedString());
assertEquals(year, rawVersion.getYear());
assertEquals(week, rawVersion.getWeek());
assertEquals(suffix, rawVersion.getSuffix());
assertFalse(rawVersion.isUnobfuscated());
var unobfuscated = raw + "_unobfuscated";
var unobfuscatedVersion = (GameVersionNumber.LegacySnapshot) asGameVersion(unobfuscated);
assertInstanceOf(GameVersionNumber.LegacySnapshot.class, rawVersion);
assertEquals(unobfuscated, unobfuscatedVersion.toString());
assertEquals(unobfuscated, unobfuscatedVersion.toNormalizedString());
assertEquals(year, unobfuscatedVersion.getYear());
assertEquals(week, unobfuscatedVersion.getWeek());
assertEquals(suffix, unobfuscatedVersion.getSuffix());
assertTrue(unobfuscatedVersion.isUnobfuscated());
var unobfuscated2 = raw + " Unobfuscated";
var unobfuscatedVersion2 = (GameVersionNumber.LegacySnapshot) asGameVersion(unobfuscated2);
assertInstanceOf(GameVersionNumber.LegacySnapshot.class, rawVersion);
assertEquals(unobfuscated2, unobfuscatedVersion2.toString());
assertEquals(unobfuscated, unobfuscatedVersion2.toNormalizedString());
assertEquals(year, unobfuscatedVersion2.getYear());
assertEquals(week, unobfuscatedVersion2.getWeek());
assertEquals(suffix, unobfuscatedVersion2.getSuffix());
assertTrue(unobfuscatedVersion2.isUnobfuscated());
}
@Test
public void testParseNew() {
List<String> versions = readVersions();
for (String version : versions) {
assertFalse(asGameVersion(version) instanceof GameVersionNumber.Old, "version=" + version);
GameVersionNumber gameVersion = asGameVersion(version);
assertFalse(gameVersion instanceof GameVersionNumber.Old, "version=" + gameVersion.toDebugString());
}
testParseLegacySnapshot(25, 46, 'a');
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parse("2.1"));
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.LegacySnapshot.parse("1.0"));
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.LegacySnapshot.parse("1.100.1"));
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.LegacySnapshot.parse("aawbba"));
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.LegacySnapshot.parse("13w12A"));
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.LegacySnapshot.parse("13w12~"));
}
private static void assertSimpleReleaseVersion(String simpleReleaseVersion, int minor, int patch) {
private static void assertSimpleReleaseVersion(String simpleReleaseVersion, int major, int minor, int patch) {
GameVersionNumber.Release release = GameVersionNumber.Release.parseSimple(simpleReleaseVersion);
assertAll("Assert Simple Release Version " + simpleReleaseVersion,
() -> assertEquals(1, release.getMajor()),
() -> assertEquals(major, release.getMajor()),
() -> assertEquals(minor, release.getMinor()),
() -> assertEquals(patch, release.getPatch()),
() -> assertEquals(GameVersionNumber.Release.TYPE_UNKNOWN, release.getEaType()),
() -> assertEquals(GameVersionNumber.Release.ReleaseType.UNKNOWN, release.getEaType()),
() -> assertEquals(VersionNumber.ZERO, release.getEaVersion())
);
}
@Test
public void testParseSimpleRelease() {
assertSimpleReleaseVersion("1.0", 0, 0);
assertSimpleReleaseVersion("1.13", 13, 0);
assertSimpleReleaseVersion("1.21.8", 21, 8);
assertSimpleReleaseVersion("1.0", 1, 0, 0);
assertSimpleReleaseVersion("1.13", 1, 13, 0);
assertSimpleReleaseVersion("1.21.8", 1, 21, 8);
assertSimpleReleaseVersion("26.1", 26, 1, 0);
assertSimpleReleaseVersion("26.1.1", 26, 1, 1);
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("26"));
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("24.0.0"));
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("24.0"));
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("2.0"));
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("1"));
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("1..0"));
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("1.0."));
assertThrows(IllegalArgumentException.class, () -> GameVersionNumber.Release.parseSimple("1.a"));
@@ -186,13 +283,13 @@ public final class GameVersionNumberTest {
"0.0",
"1.0",
"1.99",
"1.99.1-unknown1",
"1.99.1-pre1",
"1.99.1 Pre-Release 2",
"1.99.1-rc1",
"1.99.1",
"1.100",
"1.100.1"
"1.100.1",
"26.1"
);
}
@@ -202,7 +299,6 @@ public final class GameVersionNumberTest {
"90w01a",
"90w01b",
"90w01e",
"90w01~",
"90w02a"
);
}
@@ -257,6 +353,7 @@ public final class GameVersionNumberTest {
"1.14",
"1.15.2",
"20w06a",
"20w13b",
"20w14infinite",
"20w22a",
"1.16-pre1",
@@ -273,10 +370,21 @@ public final class GameVersionNumberTest {
"24w13a",
"24w14potato",
"24w14a",
"25w45a",
"25w45a_unobfuscated",
"Unknown",
"100.0"
"25w46a",
"25w46a_unobfuscated",
"1.21.11-pre1",
"1.21.11-pre1_unobfuscated",
"1.21.11-pre2",
"1.21.11-pre2_unobfuscated",
"99w99a",
"26.1-snapshot-1",
"26.1-snapshot-2",
"26.1",
"26.2-snapshot-1",
"26.2-snapshot-2",
"26.2",
"100.0",
"Unknown"
);
}
@@ -312,26 +420,84 @@ public final class GameVersionNumberTest {
);
}
private static void assertNormalized(String normalized, String version) {
assertGameVersionEquals(version);
assertGameVersionEquals(normalized, version);
assertEquals(normalized, asGameVersion(version).toNormalizedString());
}
@Test
public void testToNormalizedString() {
for (String version : readVersions()) {
assertNormalized(version, version);
}
assertNormalized("26.1-snapshot-1", "26.1 Snapshot 1");
assertNormalized("1.21.11-pre3", "1.21.11 Pre-Release 3");
assertNormalized("1.21.11-pre3_unobfuscated", "1.21.11 Pre-Release 3 Unobfuscated");
assertNormalized("1.21.11-pre3_unobfuscated", "1.21.11-pre3 Unobfuscated");
assertNormalized("1.21.11-rc1", "1.21.11 Release Candidate 1");
assertNormalized("1.21.11-rc1_unobfuscated", "1.21.11 Release Candidate 1 Unobfuscated");
assertNormalized("1.14_combat-212796", "1.14.3 - Combat Test");
assertNormalized("1.14_combat-0", "Combat Test 2");
assertNormalized("1.14_combat-3", "Combat Test 3");
assertNormalized("1.15_combat-1", "Combat Test 4");
assertNormalized("1.15_combat-6", "Combat Test 5");
assertNormalized("1.16_combat-0", "Combat Test 6");
assertNormalized("1.16_combat-1", "Combat Test 7");
assertNormalized("1.16_combat-2", "Combat Test 7b");
assertNormalized("1.16_combat-3", "Combat Test 7c");
assertNormalized("1.16_combat-4", "Combat Test 8");
assertNormalized("1.16_combat-5", "Combat Test 8b");
assertNormalized("1.16_combat-6", "Combat Test 8c");
assertNormalized("1.18_experimental-snapshot-1", "1.18 Experimental Snapshot 1");
assertNormalized("1.18_experimental-snapshot-2", "1.18 Experimental Snapshot 2");
assertNormalized("1.18_experimental-snapshot-3", "1.18 Experimental Snapshot 3");
assertNormalized("1.18_experimental-snapshot-4", "1.18 Experimental Snapshot 4");
assertNormalized("1.18_experimental-snapshot-5", "1.18 Experimental Snapshot 5");
assertNormalized("1.18_experimental-snapshot-6", "1.18 Experimental Snapshot 6");
assertNormalized("1.19_deep_dark_experimental_snapshot-1", "Deep Dark Experimental Snapshot 1");
assertNormalized("20w14infinite", "20w14~");
}
@Test
public void isAtLeast() {
assertTrue(asGameVersion("1.13").isAtLeast("1.13", "17w43a"));
assertTrue(asGameVersion("1.13.1").isAtLeast("1.13", "17w43a"));
assertTrue(asGameVersion("1.14").isAtLeast("1.13", "17w43a"));
assertTrue(asGameVersion("1.13-rc1").isAtLeast("1.13", "17w43a"));
assertTrue(asGameVersion("1.13-pre1").isAtLeast("1.13", "17w43a"));
assertTrue(asGameVersion("17w43a").isAtLeast("1.13", "17w43a"));
assertTrue(asGameVersion("17w43b").isAtLeast("1.13", "17w43a"));
assertTrue(asGameVersion("17w45a").isAtLeast("1.13", "17w43a"));
assertTrue(asGameVersion("1.13").isAtLeast("1.13", "17w43a", true));
assertTrue(asGameVersion("1.13").isAtLeast("1.13", "17w43a", false));
assertTrue(asGameVersion("1.13.1").isAtLeast("1.13", "17w43a", true));
assertTrue(asGameVersion("1.13.1").isAtLeast("1.13", "17w43a", false));
assertTrue(asGameVersion("1.14").isAtLeast("1.13", "17w43a", true));
assertTrue(asGameVersion("1.14").isAtLeast("1.13", "17w43a", false));
assertTrue(asGameVersion("1.13-rc1").isAtLeast("1.13", "17w43a", false));
assertTrue(asGameVersion("1.13-pre1").isAtLeast("1.13", "17w43a", false));
assertTrue(asGameVersion("17w43a").isAtLeast("1.13", "17w43a", true));
assertTrue(asGameVersion("17w43a").isAtLeast("1.13", "17w43a", false));
assertTrue(asGameVersion("17w43b").isAtLeast("1.13", "17w43a", true));
assertTrue(asGameVersion("17w43b").isAtLeast("1.13", "17w43a", false));
assertTrue(asGameVersion("17w45a").isAtLeast("1.13", "17w43a", true));
assertTrue(asGameVersion("17w45a").isAtLeast("1.13", "17w43a", false));
assertFalse(asGameVersion("17w31a").isAtLeast("1.13", "17w43a"));
assertFalse(asGameVersion("1.12").isAtLeast("1.13", "17w43a"));
assertFalse(asGameVersion("1.12.2").isAtLeast("1.13", "17w43a"));
assertFalse(asGameVersion("1.12.2-pre1").isAtLeast("1.13", "17w43a"));
assertFalse(asGameVersion("rd-132211").isAtLeast("1.13", "17w43a"));
assertFalse(asGameVersion("a1.0.6").isAtLeast("1.13", "17w43a"));
assertThrows(IllegalArgumentException.class, () -> asGameVersion("1.13").isAtLeast("17w43a", "17w43a"));
assertThrows(IllegalArgumentException.class, () -> asGameVersion("17w43a").isAtLeast("1.13", "1.13"));
assertThrows(IllegalArgumentException.class, () -> asGameVersion("17w43a").isAtLeast("1.13", "22w13oneblockatatime"));
assertFalse(asGameVersion("1.13-rc1").isAtLeast("1.13", "17w43a", true));
assertFalse(asGameVersion("1.13-pre1").isAtLeast("1.13", "17w43a", true));
assertFalse(asGameVersion("17w31a").isAtLeast("1.13", "17w43a", true));
assertFalse(asGameVersion("17w31a").isAtLeast("1.13", "17w43a", false));
assertFalse(asGameVersion("1.12").isAtLeast("1.13", "17w43a", true));
assertFalse(asGameVersion("1.12").isAtLeast("1.13", "17w43a", false));
assertFalse(asGameVersion("1.12.2").isAtLeast("1.13", "17w43a", true));
assertFalse(asGameVersion("1.12.2").isAtLeast("1.13", "17w43a", false));
assertFalse(asGameVersion("1.12.2-pre1").isAtLeast("1.13", "17w43a", true));
assertFalse(asGameVersion("1.12.2-pre1").isAtLeast("1.13", "17w43a", false));
assertFalse(asGameVersion("rd-132211").isAtLeast("1.13", "17w43a", true));
assertFalse(asGameVersion("rd-132211").isAtLeast("1.13", "17w43a", false));
assertFalse(asGameVersion("a1.0.6").isAtLeast("1.13", "17w43a", true));
assertFalse(asGameVersion("a1.0.6").isAtLeast("1.13", "17w43a", false));
assertThrows(IllegalArgumentException.class, () -> asGameVersion("1.13").isAtLeast("17w43a", "17w43a", true));
assertThrows(IllegalArgumentException.class, () -> asGameVersion("1.13").isAtLeast("17w43a", "17w43a", false));
assertThrows(IllegalArgumentException.class, () -> asGameVersion("17w43a").isAtLeast("1.13", "1.13", true));
assertThrows(IllegalArgumentException.class, () -> asGameVersion("17w43a").isAtLeast("1.13", "1.13", false));
assertThrows(IllegalArgumentException.class, () -> asGameVersion("17w43a").isAtLeast("1.13", "22w13oneblockatatime", true));
assertThrows(IllegalArgumentException.class, () -> asGameVersion("17w43a").isAtLeast("1.13", "22w13oneblockatatime", false));
}
}
Loaded 100 of 109 files, more files were not shown because too many files have changed in this diff. Show more