Compare commits

..
40 Commits
Author SHA1 Message Date
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
89 changed files with 2593 additions and 912 deletions

No files matched your search

-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
+18
View File
@@ -0,0 +1,18 @@
name: Sync to CNB
on:
push
jobs:
run:
if: ${{ github.repository == 'HMCL-dev/HMCL' }}
runs-on: ubuntu-latest
steps:
- name: Mirror GitHub to CNB
env:
SOURCE_REPO: HMCL-dev/HMCL
TARGET_REPO: HMCL-dev/HMCL
run: |
git clone --mirror "https://github.com/$SOURCE_REPO.git" -- repo
cd repo
git push -f --prune "https://${{ secrets.CNB_SYNC_USERNAME }}:${{ secrets.CNB_SYNC_TOKEN }}@cnb.cool/$TARGET_REPO.git" "refs/heads/*:refs/heads/*" "refs/tags/*:refs/tags/*"
+120
View File
@@ -0,0 +1,120 @@
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: |
~/go/bin/git-cnb release create \
--repo "$CNB_REPO" \
--tag "v$HMCL_VERSION" \
--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 "v$HMCL_VERSION" --file-name "HMCL-$HMCL_VERSION.jar"
echo "Uploading HMCL-$HMCL_VERSION.exe"
~/go/bin/git-cnb release asset-upload --repo="$CNB_REPO" --tag-name "v$HMCL_VERSION" --file-name "HMCL-$HMCL_VERSION.exe"
echo "Uploading HMCL-$HMCL_VERSION.sh"
~/go/bin/git-cnb release asset-upload --repo="$CNB_REPO" --tag-name "v$HMCL_VERSION" --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.*;
@@ -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);
}
@@ -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()
@@ -428,6 +428,7 @@ 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())
@@ -460,7 +461,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;
@@ -81,6 +82,7 @@ public final class LauncherHelper {
private final VersionSetting setting;
private LauncherVisibility launcherVisibility;
private boolean showLogs;
private boolean disableOfflineSkin = false;
public LauncherHelper(Profile profile, Account account, String selectedVersion) {
this.profile = Objects.requireNonNull(profile);
@@ -111,6 +113,10 @@ public final class LauncherHelper {
launcherVisibility = LauncherVisibility.KEEP;
}
public void setDisableOfflineSkin() {
disableOfflineSkin = true;
}
public void launch() {
FXUtils.checkFxUserThread();
@@ -188,8 +194,12 @@ 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);
}
LaunchOptions launchOptions = launchOptionsBuilder.create();
LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModsDirectory(selectedVersion), 10));
@@ -639,10 +649,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())) {
@@ -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() {
}
}
@@ -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);
@@ -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 {
@@ -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();
@@ -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,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.selectedProperty().bindBidirectional(config().acceptPreviewUpdateProperty());
FXUtils.installFastTooltip(previewPane, i18n("update.preview.tooltip"));
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-license");
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-license");
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) {
@@ -626,9 +630,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());
}
}
});
@@ -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());
@@ -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);
}
@@ -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"));
}
@@ -126,7 +126,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 -> {
@@ -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());
@@ -33,7 +33,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;
@@ -84,7 +83,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);
}
@@ -102,7 +101,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));
}
@@ -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;
}
+205 -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 {
@@ -289,6 +311,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 +325,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 +346,10 @@
-fx-font-size: 15px;
}
.terracotta-license Text {
-fx-fill: -monet-on-surface;
}
.game-crash-window {
-fx-background-color: -monet-surface-container;
}
@@ -329,7 +359,7 @@
}
.game-crash-window .crash-reason-text-flow .hyperlink {
-fx-fill: #0070E0;
-fx-fill: -monet-primary;
}
.wrap-text > HBox > .subtitle {
@@ -337,14 +367,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 +441,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 +675,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 +759,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 +769,7 @@
}
.jfx-button-raised .jfx-rippler {
-jfx-rippler-fill: -monet-on-primary;
-jfx-mask-type: CIRCLE;
-fx-padding: 0.0;
}
@@ -795,6 +813,10 @@
-jfx-unchecked-color: transparent;
}
.table-view .jfx-check-box .jfx-rippler {
-jfx-rippler-disabled: true;
}
/*******************************************************************************
* *
* JFX Progress Bar *
@@ -826,6 +848,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;
@@ -1194,28 +1217,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 +1280,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 +1424,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 +1473,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 +1496,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 +1580,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 +1606,6 @@
* Scroll Bar *
* *
******************************************************************************/
/* unsued? */
.scroll-bar:vertical > .track-background, .scroll-bar:horizontal > .track-background {
-fx-background-color: #F1F1F1;
@@ -1518,6 +1638,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 +1726,7 @@
}
.html-hyperlink {
-fx-fill: blue;
-fx-fill: -monet-primary;
}
.html-h1 {
@@ -1620,3 +1748,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;
}
@@ -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
@@ -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=實例遊戲設定類型
@@ -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=实例游戏设置类型
@@ -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);
}
}
@@ -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;
/**
@@ -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.
*/
@@ -497,5 +502,9 @@ public class LaunchOptions implements Serializable {
return this;
}
public Builder setEnableDebugLogOutput(boolean u) {
options.enableDebugLogOutput = u;
return this;
}
}
}
@@ -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()));
}
@@ -421,14 +421,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);
}
}
@@ -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));
}
}
+1
View File
@@ -2,6 +2,7 @@
<!-- #BEGIN BLOCK -->
<!-- #PROPERTY NAME=BADGES -->
[![CNB](https://img.shields.io/badge/cnb-mirror-ff6200?logo=cloudnativebuild)](https://cnb.cool/HMCL-dev/HMCL)
[![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?label=Downloads&style=flat)](https://github.com/HMCL-dev/HMCL/releases)
![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat)
[![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U)
+1
View File
@@ -2,6 +2,7 @@
<!-- #BEGIN COPY -->
<!-- #PROPERTY NAME=BADGES -->
[![CNB](https://img.shields.io/badge/cnb-mirror-ff6200?logo=cloudnativebuild)](https://cnb.cool/HMCL-dev/HMCL)
[![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?label=Downloads&style=flat)](https://github.com/HMCL-dev/HMCL/releases)
![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat)
[![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U)
+1
View File
@@ -2,6 +2,7 @@
<!-- #BEGIN COPY -->
<!-- #PROPERTY NAME=BADGES -->
[![CNB](https://img.shields.io/badge/cnb-mirror-ff6200?logo=cloudnativebuild)](https://cnb.cool/HMCL-dev/HMCL)
[![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?label=Downloads&style=flat)](https://github.com/HMCL-dev/HMCL/releases)
![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat)
[![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U)
+1
View File
@@ -2,6 +2,7 @@
<!-- #BEGIN COPY -->
<!-- #PROPERTY NAME=BADGES -->
[![CNB](https://img.shields.io/badge/cnb-mirror-ff6200?logo=cloudnativebuild)](https://cnb.cool/HMCL-dev/HMCL)
[![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?label=Downloads&style=flat)](https://github.com/HMCL-dev/HMCL/releases)
![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat)
[![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U)
+1
View File
@@ -2,6 +2,7 @@
<!-- #BEGIN COPY -->
<!-- #PROPERTY NAME=BADGES -->
[![CNB](https://img.shields.io/badge/cnb-mirror-ff6200?logo=cloudnativebuild)](https://cnb.cool/HMCL-dev/HMCL)
[![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?label=Downloads&style=flat)](https://github.com/HMCL-dev/HMCL/releases)
![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat)
[![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U)
+1
View File
@@ -2,6 +2,7 @@
<!-- #BEGIN COPY -->
<!-- #PROPERTY NAME=BADGES -->
[![CNB](https://img.shields.io/badge/cnb-mirror-ff6200?logo=cloudnativebuild)](https://cnb.cool/HMCL-dev/HMCL)
[![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?label=Downloads&style=flat)](https://github.com/HMCL-dev/HMCL/releases)
![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat)
[![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U)
+1
View File
@@ -2,6 +2,7 @@
<!-- #BEGIN COPY -->
<!-- #PROPERTY NAME=BADGES -->
[![CNB](https://img.shields.io/badge/cnb-mirror-ff6200?logo=cloudnativebuild)](https://cnb.cool/HMCL-dev/HMCL)
[![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?label=Downloads&style=flat)](https://github.com/HMCL-dev/HMCL/releases)
![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat)
[![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U)
+1
View File
@@ -2,6 +2,7 @@
<!-- #BEGIN COPY -->
<!-- #PROPERTY NAME=BADGES -->
[![CNB](https://img.shields.io/badge/cnb-mirror-ff6200?logo=cloudnativebuild)](https://cnb.cool/HMCL-dev/HMCL)
[![Downloads](https://img.shields.io/github/downloads/HMCL-dev/HMCL/total?label=Downloads&style=flat)](https://github.com/HMCL-dev/HMCL/releases)
![Stars](https://img.shields.io/github/stars/HMCL-dev/HMCL?style=flat)
[![Discord](https://img.shields.io/discord/995291757799538688.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/jVvC7HfM6U)
+1 -1
View File
@@ -16,7 +16,7 @@ twelvemonkeys = "3.12.0"
jna = "5.18.1"
pci-ids = "0.4.0"
java-info = "1.0"
authlib-injector = "1.2.6"
authlib-injector = "1.2.7"
monet-fx = "0.4.0"
# testing