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