Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2d8d9f93f | ||
|
|
da4ed04697 | ||
|
|
5d44df6b40 | ||
|
|
114503e0bf | ||
|
|
8bd88aa8d8 | ||
|
|
cc7d1e1aae | ||
|
|
56d4effda0 | ||
|
|
7c2dbe3dae | ||
|
|
99469077d2 | ||
|
|
ec930843da | ||
|
|
969d314802 | ||
|
|
3b11541b0a | ||
|
|
c58d5a3a81 | ||
|
|
cf9b4aacb2 | ||
|
|
e430db0cd6 | ||
|
|
1b31e96629 | ||
|
|
5c19239ddd | ||
|
|
c181c7825b | ||
|
|
80d2fefa1f | ||
|
|
8701030934 | ||
|
|
ae4c152a8b | ||
|
|
e436163be7 | ||
|
|
9b8e65449b | ||
|
|
fbb52f4e8a | ||
|
|
704eb32048 | ||
|
|
730d9b4ce8 | ||
|
|
c7b316ecba | ||
|
|
6df5a02c3f | ||
|
|
1b9fde3d60 | ||
|
|
f435cda6a3 | ||
|
|
4a69fde6e6 | ||
|
|
294a0cd7da | ||
|
|
e057e20b3c |
No files matched your search
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: QQ 群
|
||||
url: https://docs.hmcl.net/groups.html
|
||||
about: Hello Minecraft! Launcher 的官方 QQ 交流群。
|
||||
@@ -14,9 +14,9 @@ jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
|
||||
@@ -12,9 +12,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
@@ -31,23 +31,20 @@ jobs:
|
||||
- name: Get short SHA
|
||||
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
- name: Upload JAR
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: HMCL-${{ env.SHORT_SHA }}-jar
|
||||
path: |
|
||||
HMCL/build/libs/HMCL-*.jar
|
||||
HMCL/build/libs/HMCL-*.jar.sha256
|
||||
path: HMCL/build/libs/HMCL-*.jar
|
||||
archive: false
|
||||
- name: Upload EXE
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: HMCL-${{ env.SHORT_SHA }}-exe
|
||||
path: |
|
||||
HMCL/build/libs/HMCL-*.exe
|
||||
HMCL/build/libs/HMCL-*.exe.sha256
|
||||
path: HMCL/build/libs/HMCL-*.exe
|
||||
archive: false
|
||||
- name: Upload SH
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: HMCL-${{ env.SHORT_SHA }}-sh
|
||||
path: |
|
||||
HMCL/build/libs/HMCL-*.sh
|
||||
HMCL/build/libs/HMCL-*.sh.sha256
|
||||
path: HMCL/build/libs/HMCL-*.sh
|
||||
archive: false
|
||||
@@ -49,7 +49,6 @@ import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BackgroundFill;
|
||||
import javafx.scene.layout.CornerRadii;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
@@ -155,7 +154,6 @@ public class JFXDialog extends StackPane {
|
||||
});
|
||||
|
||||
contentHolder = new StackPane();
|
||||
contentHolder.setBackground(new Background(new BackgroundFill(Color.WHITE, new CornerRadii(2), null)));
|
||||
JFXDepthManager.setDepth(contentHolder, 4);
|
||||
contentHolder.setPickOnBounds(false);
|
||||
// ensure stackpane is never resized beyond it's preferred size
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
package com.jfoenix.controls;
|
||||
|
||||
import com.jfoenix.skins.JFXProgressBarSkin;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.scene.control.ProgressBar;
|
||||
import javafx.scene.control.Skin;
|
||||
|
||||
@@ -51,26 +49,18 @@ public class JFXProgressBar extends ProgressBar {
|
||||
return new JFXProgressBarSkin(this);
|
||||
}
|
||||
|
||||
private boolean smoothProgress = true;
|
||||
|
||||
public boolean isSmoothProgress() {
|
||||
return smoothProgress;
|
||||
}
|
||||
|
||||
public void setSmoothProgress(boolean smoothProgress) {
|
||||
this.smoothProgress = smoothProgress;
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
setPrefWidth(200);
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
|
||||
|
||||
private DoubleProperty secondaryProgress;
|
||||
|
||||
public DoubleProperty secondaryProgressProperty() {
|
||||
if (secondaryProgress == null) {
|
||||
secondaryProgress = new SimpleDoubleProperty(this, "secondaryProgress", INDETERMINATE_PROGRESS);
|
||||
}
|
||||
return secondaryProgress;
|
||||
}
|
||||
|
||||
public double getSecondaryProgress() {
|
||||
return secondaryProgress == null ? INDETERMINATE_PROGRESS : secondaryProgress.get();
|
||||
}
|
||||
|
||||
public void setSecondaryProgress(double secondaryProgress) {
|
||||
secondaryProgressProperty().set(secondaryProgress);
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import javafx.scene.control.Skin;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/// JFXSpinner is the material design implementation of a loading spinner.
|
||||
///
|
||||
@@ -71,10 +72,10 @@ public class JFXSpinner extends ProgressIndicator {
|
||||
/// this control.
|
||||
private static final String DEFAULT_STYLE_CLASS = "jfx-spinner";
|
||||
|
||||
private static final double DEFAULT_RADIUS = 12.0;
|
||||
private static final double DEFAULT_RADIUS = 16.0;
|
||||
|
||||
/**
|
||||
* specifies the radius of the spinner node, by default it's set to `12.0`
|
||||
* specifies the radius of the spinner node, by default it's set to `16.0`
|
||||
*/
|
||||
private StyleableDoubleProperty radius;
|
||||
|
||||
@@ -96,25 +97,8 @@ public class JFXSpinner extends ProgressIndicator {
|
||||
this.radiusProperty().set(radius);
|
||||
}
|
||||
|
||||
/// specifies from which angle the spinner should start spinning
|
||||
private StyleableDoubleProperty startingAngle;
|
||||
|
||||
public final StyleableDoubleProperty startingAngleProperty() {
|
||||
if (this.startingAngle == null) {
|
||||
startingAngle = new SimpleStyleableDoubleProperty(StyleableProperties.STARTING_ANGLE,
|
||||
JFXSpinner.this,
|
||||
"startingAngle",
|
||||
0.0);
|
||||
}
|
||||
return this.startingAngle;
|
||||
}
|
||||
|
||||
public final double getStartingAngle() {
|
||||
return startingAngle != null ? startingAngle.get() : 0.0;
|
||||
}
|
||||
|
||||
public final void setStartingAngle(final double startingAngle) {
|
||||
this.startingAngleProperty().set(startingAngle);
|
||||
public double getStartingAngle() {
|
||||
return 360 - ThreadLocalRandom.current().nextDouble() * 720;
|
||||
}
|
||||
|
||||
private static final class StyleableProperties {
|
||||
@@ -132,26 +116,12 @@ public class JFXSpinner extends ProgressIndicator {
|
||||
}
|
||||
};
|
||||
|
||||
private static final CssMetaData<JFXSpinner, Number> STARTING_ANGLE =
|
||||
new CssMetaData<>("-jfx-starting-angle",
|
||||
SizeConverter.getInstance(), 0.0) {
|
||||
@Override
|
||||
public boolean isSettable(JFXSpinner control) {
|
||||
return control.startingAngle == null || !control.startingAngle.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StyleableDoubleProperty getStyleableProperty(JFXSpinner control) {
|
||||
return control.startingAngleProperty();
|
||||
}
|
||||
};
|
||||
|
||||
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
|
||||
|
||||
static {
|
||||
final List<CssMetaData<? extends Styleable, ?>> styleables =
|
||||
new ArrayList<>(ProgressIndicator.getClassCssMetaData());
|
||||
Collections.addAll(styleables, RADIUS, STARTING_ANGLE);
|
||||
Collections.addAll(styleables, RADIUS);
|
||||
CHILD_STYLEABLES = List.copyOf(styleables);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,14 @@ public class JFXTextField extends TextField {
|
||||
useJFXContextMenu(this);
|
||||
}
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/5822
|
||||
// TODO: This method may no longer be needed after we update JFXTextFieldSkin
|
||||
@Override
|
||||
protected void layoutChildren() {
|
||||
super.layoutChildren();
|
||||
this.setNeedsLayout(false);
|
||||
}
|
||||
|
||||
/***************************************************************************
|
||||
* *
|
||||
* Properties *
|
||||
|
||||
@@ -179,7 +179,7 @@ public class JFXCustomColorPickerDialog extends StackPane {
|
||||
(int) (newColor.getRed() * 255),
|
||||
(int) (newColor.getGreen() * 255),
|
||||
(int) (newColor.getBlue() * 255));
|
||||
String rgb = String.format("rgba(%d, %d, %d, 1)",
|
||||
String rgb = String.format("rgb(%d, %d, %d)",
|
||||
(int) (newColor.getRed() * 255),
|
||||
(int) (newColor.getGreen() * 255),
|
||||
(int) (newColor.getBlue() * 255));
|
||||
@@ -294,7 +294,11 @@ public class JFXCustomColorPickerDialog extends StackPane {
|
||||
if (!systemChange) {
|
||||
userChange = true;
|
||||
try {
|
||||
curvedColorPicker.setColor(Color.valueOf(colorWebString));
|
||||
Color color = Color.valueOf(colorWebString);
|
||||
if (color.getOpacity() != 1.0) {
|
||||
color = Color.color(color.getRed(), color.getGreen(), color.getBlue());
|
||||
}
|
||||
curvedColorPicker.setColor(color);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
// if color is not valid then do nothing
|
||||
}
|
||||
|
||||
@@ -20,61 +20,38 @@
|
||||
package com.jfoenix.skins;
|
||||
|
||||
import com.jfoenix.controls.JFXProgressBar;
|
||||
import com.jfoenix.utils.JFXNodeUtils;
|
||||
import com.jfoenix.utils.TreeShowingProperty;
|
||||
import javafx.animation.*;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.control.skin.ProgressIndicatorSkin;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
|
||||
/// # Material Design ProgressBar Skin
|
||||
///
|
||||
/// @author Shadi Shaheen
|
||||
/// @version 2.0
|
||||
/// @since 2017-10-06
|
||||
public class JFXProgressBarSkin extends ProgressIndicatorSkin {
|
||||
public class JFXProgressBarSkin extends SkinBase<JFXProgressBar> {
|
||||
|
||||
private StackPane track;
|
||||
private StackPane secondaryBar;
|
||||
private StackPane bar;
|
||||
private double barWidth = 0;
|
||||
private double secondaryBarWidth = 0;
|
||||
private Timeline indeterminateTransition;
|
||||
private Region clip;
|
||||
private static final double DEFAULT_HEIGHT = 4;
|
||||
|
||||
private final StackPane track;
|
||||
private final StackPane bar;
|
||||
private final Rectangle clip;
|
||||
private Animation transition;
|
||||
private final TreeShowingProperty treeShowingProperty;
|
||||
private double fullWidth;
|
||||
|
||||
public JFXProgressBarSkin(JFXProgressBar bar) {
|
||||
super(bar);
|
||||
public JFXProgressBarSkin(JFXProgressBar control) {
|
||||
super(control);
|
||||
|
||||
this.treeShowingProperty = new TreeShowingProperty(bar);
|
||||
this.treeShowingProperty = new TreeShowingProperty(control);
|
||||
|
||||
bar.widthProperty().addListener(observable -> {
|
||||
updateProgress();
|
||||
updateSecondaryProgress();
|
||||
});
|
||||
|
||||
registerChangeListener(bar.progressProperty(), (obs) -> updateProgress());
|
||||
registerChangeListener(bar.secondaryProgressProperty(), obs -> updateSecondaryProgress());
|
||||
registerChangeListener(bar.visibleProperty(), obs -> updateAnimation());
|
||||
registerChangeListener(bar.parentProperty(), obs -> updateAnimation());
|
||||
registerChangeListener(bar.sceneProperty(), obs -> updateAnimation());
|
||||
|
||||
unregisterChangeListeners(treeShowingProperty);
|
||||
unregisterChangeListeners(bar.indeterminateProperty());
|
||||
|
||||
registerChangeListener(treeShowingProperty, obs -> this.updateAnimation());
|
||||
registerChangeListener(bar.indeterminateProperty(), obs -> initialize());
|
||||
|
||||
initialize();
|
||||
|
||||
getSkinnable().requestLayout();
|
||||
}
|
||||
|
||||
protected void initialize() {
|
||||
registerChangeListener(treeShowingProperty, obs -> updateProgress(false));
|
||||
registerChangeListener(control.progressProperty(), obs -> updateProgress(true));
|
||||
|
||||
track = new StackPane();
|
||||
track.getStyleClass().setAll("track");
|
||||
@@ -82,14 +59,14 @@ public class JFXProgressBarSkin extends ProgressIndicatorSkin {
|
||||
bar = new StackPane();
|
||||
bar.getStyleClass().setAll("bar");
|
||||
|
||||
secondaryBar = new StackPane();
|
||||
secondaryBar.getStyleClass().setAll("secondary-bar");
|
||||
clip = new Rectangle();
|
||||
clip.setArcWidth(DEFAULT_HEIGHT);
|
||||
clip.setArcHeight(DEFAULT_HEIGHT);
|
||||
bar.setClip(clip);
|
||||
|
||||
clip = new Region();
|
||||
clip.setBackground(new Background(new BackgroundFill(Color.BLACK, CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
bar.backgroundProperty().addListener(observable -> JFXNodeUtils.updateBackground(bar.getBackground(), clip));
|
||||
getChildren().setAll(track, bar);
|
||||
|
||||
getChildren().setAll(track, secondaryBar, bar);
|
||||
getSkinnable().requestLayout();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -104,7 +81,7 @@ public class JFXProgressBarSkin extends ProgressIndicatorSkin {
|
||||
|
||||
@Override
|
||||
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
return topInset + bar.prefHeight(width) + bottomInset;
|
||||
return topInset + DEFAULT_HEIGHT + bottomInset;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -120,105 +97,129 @@ public class JFXProgressBarSkin extends ProgressIndicatorSkin {
|
||||
@Override
|
||||
protected void layoutChildren(double x, double y, double w, double h) {
|
||||
track.resizeRelocate(x, y, w, h);
|
||||
secondaryBar.resizeRelocate(x, y, secondaryBarWidth, h);
|
||||
bar.resizeRelocate(x, y, getSkinnable().isIndeterminate() ? w : barWidth, h);
|
||||
clip.resizeRelocate(0, 0, w, h);
|
||||
bar.resizeRelocate(x, y, w, h);
|
||||
|
||||
if (getSkinnable().isIndeterminate()) {
|
||||
createIndeterminateTimeline();
|
||||
if (JFXNodeUtils.isTreeShowing(getSkinnable())) {
|
||||
indeterminateTransition.play();
|
||||
}
|
||||
// apply clip
|
||||
bar.setClip(clip);
|
||||
} else if (indeterminateTransition != null) {
|
||||
clip.relocate(0, 0);
|
||||
clip.setWidth(0);
|
||||
clip.setHeight(h);
|
||||
clip.setTranslateX(0);
|
||||
|
||||
fullWidth = w;
|
||||
|
||||
clearAnimation();
|
||||
updateProgress(false);
|
||||
}
|
||||
|
||||
private boolean wasIndeterminate = false;
|
||||
|
||||
private void updateProgress(boolean playProgressAnimation) {
|
||||
double progress = Math.min(getSkinnable().getProgress(), 1.0);
|
||||
boolean isIndeterminate = progress < 0.0;
|
||||
boolean isTreeShowing = treeShowingProperty.get();
|
||||
|
||||
if (isIndeterminate != wasIndeterminate) {
|
||||
wasIndeterminate = isIndeterminate;
|
||||
clearAnimation();
|
||||
// remove clip
|
||||
bar.setClip(null);
|
||||
clip.setTranslateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
protected void updateSecondaryProgress() {
|
||||
final JFXProgressBar control = (JFXProgressBar) getSkinnable();
|
||||
secondaryBarWidth = ((int) (control.getWidth() - snappedLeftInset() - snappedRightInset()) * 2
|
||||
* Math.min(1, Math.max(0, control.getSecondaryProgress()))) / 2.0F;
|
||||
control.requestLayout();
|
||||
}
|
||||
|
||||
boolean wasIndeterminate = false;
|
||||
|
||||
protected void pauseTimeline(boolean pause) {
|
||||
if (getSkinnable().isIndeterminate()) {
|
||||
if (indeterminateTransition == null) {
|
||||
createIndeterminateTimeline();
|
||||
if (isIndeterminate) { // indeterminate
|
||||
if (isTreeShowing) {
|
||||
if (transition == null) {
|
||||
transition = createIndeterminateTransition();
|
||||
transition.playFromStart();
|
||||
} else {
|
||||
transition.play();
|
||||
}
|
||||
} else if (transition != null) {
|
||||
transition.pause();
|
||||
}
|
||||
if (pause) {
|
||||
indeterminateTransition.pause();
|
||||
} else { // determinate
|
||||
clearAnimation();
|
||||
if (isTreeShowing && playProgressAnimation
|
||||
&& AnimationUtils.isAnimationEnabled()
|
||||
&& getSkinnable().isSmoothProgress()) {
|
||||
transition = createDeterminateTransition(progress);
|
||||
transition.playFromStart();
|
||||
} else {
|
||||
indeterminateTransition.play();
|
||||
clip.setWidth(computeBarWidth(progress));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAnimation() {
|
||||
final boolean isTreeShowing = treeShowingProperty.get();
|
||||
if (indeterminateTransition != null) {
|
||||
pauseTimeline(!isTreeShowing);
|
||||
} else if (isTreeShowing) {
|
||||
createIndeterminateTimeline();
|
||||
}
|
||||
private static final Duration INDETERMINATE_DURATION = Duration.seconds(1);
|
||||
|
||||
private Transition createIndeterminateTransition() {
|
||||
double minWidth = 0;
|
||||
double maxWidth = fullWidth * 0.4;
|
||||
Transition transition = new Transition() {
|
||||
{
|
||||
setInterpolator(Interpolator.LINEAR);
|
||||
setCycleDuration(INDETERMINATE_DURATION);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void interpolate(double frac) {
|
||||
double currentWidth;
|
||||
|
||||
if (frac <= 0.5) {
|
||||
currentWidth = Interpolator.EASE_IN.interpolate(minWidth, maxWidth, frac / 0.5);
|
||||
} else {
|
||||
currentWidth = Interpolator.EASE_OUT.interpolate(maxWidth, minWidth, (frac - 0.5) / 0.5);
|
||||
}
|
||||
|
||||
double targetCenter;
|
||||
if (frac <= 0.1) {
|
||||
targetCenter = 0.0;
|
||||
} else if (frac >= 0.9) {
|
||||
targetCenter = fullWidth;
|
||||
} else {
|
||||
targetCenter = ((frac - 0.1) / 0.8) * fullWidth;
|
||||
}
|
||||
|
||||
clip.setWidth(currentWidth);
|
||||
clip.setTranslateX(targetCenter - currentWidth / 2.0);
|
||||
}
|
||||
};
|
||||
|
||||
transition.setCycleCount(Timeline.INDEFINITE);
|
||||
return transition;
|
||||
}
|
||||
|
||||
private void updateProgress() {
|
||||
final ProgressIndicator control = getSkinnable();
|
||||
final boolean isIndeterminate = control.isIndeterminate();
|
||||
if (!(isIndeterminate && wasIndeterminate)) {
|
||||
barWidth = ((int) (control.getWidth() - snappedLeftInset() - snappedRightInset()) * 2
|
||||
* Math.min(1, Math.max(0, control.getProgress()))) / 2.0F;
|
||||
control.requestLayout();
|
||||
}
|
||||
wasIndeterminate = isIndeterminate;
|
||||
}
|
||||
private static final Duration DETERMINATE_DURATION = Duration.seconds(0.2);
|
||||
|
||||
private void createIndeterminateTimeline() {
|
||||
if (indeterminateTransition != null) {
|
||||
clearAnimation();
|
||||
}
|
||||
double dur = 1;
|
||||
ProgressIndicator control = getSkinnable();
|
||||
final double w = control.getWidth() - (snappedLeftInset() + snappedRightInset());
|
||||
indeterminateTransition = new Timeline(new KeyFrame(
|
||||
Duration.ZERO,
|
||||
new KeyValue(clip.scaleXProperty(), 0.0, Interpolator.EASE_IN),
|
||||
new KeyValue(clip.translateXProperty(), -w / 2, Interpolator.LINEAR)
|
||||
),
|
||||
new KeyFrame(
|
||||
Duration.seconds(0.5 * dur),
|
||||
new KeyValue(clip.scaleXProperty(), 0.4, Interpolator.LINEAR)
|
||||
),
|
||||
new KeyFrame(
|
||||
Duration.seconds(0.9 * dur),
|
||||
new KeyValue(clip.translateXProperty(), w / 2, Interpolator.LINEAR)
|
||||
),
|
||||
new KeyFrame(
|
||||
Duration.seconds(1 * dur),
|
||||
new KeyValue(clip.scaleXProperty(), 0.0, Interpolator.EASE_OUT)
|
||||
));
|
||||
indeterminateTransition.setCycleCount(Timeline.INDEFINITE);
|
||||
private Timeline createDeterminateTransition(double targetProgress) {
|
||||
Timeline timeline = new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(clip.widthProperty(), clip.getWidth(), Interpolator.EASE_OUT)),
|
||||
new KeyFrame(DETERMINATE_DURATION,
|
||||
new KeyValue(clip.widthProperty(), computeBarWidth(targetProgress), Interpolator.EASE_OUT))
|
||||
);
|
||||
timeline.setOnFinished(e -> {
|
||||
if (transition == timeline) {
|
||||
transition = null;
|
||||
}
|
||||
});
|
||||
return timeline;
|
||||
}
|
||||
|
||||
private void clearAnimation() {
|
||||
indeterminateTransition.stop();
|
||||
indeterminateTransition.getKeyFrames().clear();
|
||||
indeterminateTransition = null;
|
||||
if (transition != null) {
|
||||
transition.stop();
|
||||
transition = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
treeShowingProperty.dispose();
|
||||
if (indeterminateTransition != null) {
|
||||
clearAnimation();
|
||||
}
|
||||
clearAnimation();
|
||||
}
|
||||
|
||||
private double computeBarWidth(double progress) {
|
||||
assert progress >= 0 && progress <= 1;
|
||||
double barWidth = ((int) fullWidth * 2 * progress) / 2.0;
|
||||
return progress > 0 ? Math.max(barWidth, DEFAULT_HEIGHT) : barWidth;
|
||||
}
|
||||
}
|
||||
@@ -20,25 +20,24 @@
|
||||
package com.jfoenix.skins;
|
||||
|
||||
import com.jfoenix.controls.JFXSpinner;
|
||||
import com.jfoenix.utils.JFXNodeUtils;
|
||||
import com.jfoenix.utils.TreeShowingProperty;
|
||||
import javafx.animation.Interpolator;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.Arc;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.shape.StrokeLineCap;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/// JFXSpinner material design skin
|
||||
///
|
||||
@@ -47,10 +46,7 @@ import javafx.util.Duration;
|
||||
/// @since 2017-09-25
|
||||
public class JFXSpinnerSkin extends SkinBase<JFXSpinner> {
|
||||
|
||||
private static final Color GREEN_COLOR = Color.valueOf("#0F9D58");
|
||||
private static final Color RED_COLOR = Color.valueOf("#db4437");
|
||||
private static final Color YELLOW_COLOR = Color.valueOf("#f4b400");
|
||||
private static final Color BLUE_COLOR = Color.valueOf("#4285f4");
|
||||
private static final double DEFAULT_STROKE_WIDTH = 4;
|
||||
|
||||
private JFXSpinner control;
|
||||
private final TreeShowingProperty treeShowingProperty;
|
||||
@@ -61,110 +57,62 @@ public class JFXSpinnerSkin extends SkinBase<JFXSpinner> {
|
||||
private Arc track;
|
||||
private final StackPane arcPane;
|
||||
private final Rectangle fillRect;
|
||||
private double arcLength = -1;
|
||||
private final Text text;
|
||||
|
||||
private final double startingAngle;
|
||||
|
||||
public JFXSpinnerSkin(JFXSpinner control) {
|
||||
super(control);
|
||||
|
||||
this.control = control;
|
||||
this.treeShowingProperty = new TreeShowingProperty(control);
|
||||
this.startingAngle = control.getStartingAngle();
|
||||
|
||||
arc = new Arc();
|
||||
arc.setManaged(false);
|
||||
arc.setStartAngle(0);
|
||||
arc.setLength(180);
|
||||
arc.getStyleClass().setAll("arc");
|
||||
arc.setFill(Color.TRANSPARENT);
|
||||
arc.setStrokeWidth(3);
|
||||
arc.setStrokeWidth(DEFAULT_STROKE_WIDTH);
|
||||
arc.setStrokeLineCap(StrokeLineCap.ROUND);
|
||||
|
||||
track = new Arc();
|
||||
track.setManaged(false);
|
||||
track.setStartAngle(0);
|
||||
track.setLength(360);
|
||||
track.setStrokeWidth(3);
|
||||
track.setStrokeWidth(DEFAULT_STROKE_WIDTH);
|
||||
track.getStyleClass().setAll("track");
|
||||
track.setFill(Color.TRANSPARENT);
|
||||
|
||||
fillRect = new Rectangle();
|
||||
fillRect.setFill(Color.TRANSPARENT);
|
||||
text = new Text();
|
||||
text.setStyle("-fx-font-size:null");
|
||||
text.getStyleClass().setAll("text", "percentage");
|
||||
final Group group = new Group(fillRect, track, arc, text);
|
||||
final Group group = new Group(fillRect, track, arc);
|
||||
group.setManaged(false);
|
||||
arcPane = new StackPane(group);
|
||||
arcPane.setPrefSize(50, 50);
|
||||
getChildren().setAll(arcPane);
|
||||
|
||||
// register listeners
|
||||
registerChangeListener(control.indeterminateProperty(), obs -> initialize());
|
||||
registerChangeListener(control.progressProperty(), obs -> updateProgress());
|
||||
registerChangeListener(treeShowingProperty, obs -> updateAnimation());
|
||||
registerChangeListener(control.sceneProperty(), obs -> updateAnimation());
|
||||
registerChangeListener(treeShowingProperty, obs -> updateProgress());
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
if (getSkinnable().isIndeterminate()) {
|
||||
if (timeline == null) {
|
||||
createTransition();
|
||||
if (JFXNodeUtils.isTreeShowing(getSkinnable())) {
|
||||
private void updateProgress() {
|
||||
double progress = Double.min(getSkinnable().getProgress(), 1.0);
|
||||
if (progress < 0) { // indeterminate
|
||||
boolean treeShowing = treeShowingProperty.get();
|
||||
if (treeShowing) {
|
||||
if (timeline == null) {
|
||||
timeline = createTransition();
|
||||
timeline.playFromStart();
|
||||
} else {
|
||||
timeline.play();
|
||||
}
|
||||
} else if (timeline != null) {
|
||||
timeline.pause();
|
||||
}
|
||||
} else {
|
||||
} else { // determinate
|
||||
clearAnimation();
|
||||
arc.setStartAngle(90);
|
||||
updateProgress();
|
||||
}
|
||||
}
|
||||
|
||||
private KeyFrame[] getKeyFrames(double angle, double duration, Paint color) {
|
||||
KeyFrame[] frames = new KeyFrame[4];
|
||||
frames[0] = new KeyFrame(Duration.seconds(duration),
|
||||
new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
|
||||
new KeyValue(arc.startAngleProperty(),
|
||||
angle + 45 + control.getStartingAngle(),
|
||||
Interpolator.LINEAR));
|
||||
frames[1] = new KeyFrame(Duration.seconds(duration + 0.4),
|
||||
new KeyValue(arc.lengthProperty(), 250, Interpolator.LINEAR),
|
||||
new KeyValue(arc.startAngleProperty(),
|
||||
angle + 90 + control.getStartingAngle(),
|
||||
Interpolator.LINEAR));
|
||||
frames[2] = new KeyFrame(Duration.seconds(duration + 0.7),
|
||||
new KeyValue(arc.lengthProperty(), 250, Interpolator.LINEAR),
|
||||
new KeyValue(arc.startAngleProperty(),
|
||||
angle + 135 + control.getStartingAngle(),
|
||||
Interpolator.LINEAR));
|
||||
frames[3] = new KeyFrame(Duration.seconds(duration + 1.1),
|
||||
new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
|
||||
new KeyValue(arc.startAngleProperty(),
|
||||
angle + 435 + control.getStartingAngle(),
|
||||
Interpolator.LINEAR),
|
||||
new KeyValue(arc.strokeProperty(), color, Interpolator.EASE_BOTH));
|
||||
return frames;
|
||||
}
|
||||
|
||||
private void pauseTimeline(boolean pause) {
|
||||
if (getSkinnable().isIndeterminate()) {
|
||||
if (timeline == null) {
|
||||
createTransition();
|
||||
}
|
||||
if (pause) {
|
||||
timeline.pause();
|
||||
} else {
|
||||
timeline.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAnimation() {
|
||||
final boolean isTreeShowing = treeShowingProperty.get();
|
||||
if (timeline != null) {
|
||||
pauseTimeline(!isTreeShowing);
|
||||
} else if (isTreeShowing) {
|
||||
createTransition();
|
||||
arc.setLength(-360 * progress);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,21 +172,9 @@ public class JFXSpinnerSkin extends SkinBase<JFXSpinner> {
|
||||
fillRect.setHeight(arcSize);
|
||||
|
||||
if (!isValid) {
|
||||
initialize();
|
||||
updateProgress();
|
||||
isValid = true;
|
||||
}
|
||||
|
||||
if (!getSkinnable().isIndeterminate()) {
|
||||
arc.setLength(arcLength);
|
||||
if (text.isVisible()) {
|
||||
final double progress = control.getProgress();
|
||||
int intProgress = (int) Math.round(progress * 100.0);
|
||||
Font font = text.getFont();
|
||||
text.setFont(Font.font(font.getFamily(), radius / 1.7));
|
||||
text.setText((progress > 1 ? 100 : intProgress) + "%");
|
||||
text.relocate((arcSize - text.getLayoutBounds().getWidth()) / 2, (arcSize - text.getLayoutBounds().getHeight()) / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateArcLayout(double radius, double arcSize) {
|
||||
@@ -254,66 +190,64 @@ public class JFXSpinnerSkin extends SkinBase<JFXSpinner> {
|
||||
track.setStrokeWidth(arc.getStrokeWidth());
|
||||
}
|
||||
|
||||
boolean wasIndeterminate = false;
|
||||
|
||||
protected void updateProgress() {
|
||||
final ProgressIndicator control = getSkinnable();
|
||||
final boolean isIndeterminate = control.isIndeterminate();
|
||||
if (!(isIndeterminate && wasIndeterminate)) {
|
||||
arcLength = -360 * control.getProgress();
|
||||
control.requestLayout();
|
||||
}
|
||||
wasIndeterminate = isIndeterminate;
|
||||
}
|
||||
|
||||
private void createTransition() {
|
||||
if (!getSkinnable().isIndeterminate()) return;
|
||||
final Paint initialColor = arc.getStroke();
|
||||
if (initialColor == null) {
|
||||
arc.setStroke(BLUE_COLOR);
|
||||
}
|
||||
|
||||
KeyFrame[] blueFrame = getKeyFrames(0, 0, initialColor == null ? BLUE_COLOR : initialColor);
|
||||
KeyFrame[] redFrame = getKeyFrames(450, 1.4, initialColor == null ? RED_COLOR : initialColor);
|
||||
KeyFrame[] yellowFrame = getKeyFrames(900, 2.8, initialColor == null ? YELLOW_COLOR : initialColor);
|
||||
KeyFrame[] greenFrame = getKeyFrames(1350, 4.2, initialColor == null ? GREEN_COLOR : initialColor);
|
||||
|
||||
KeyFrame endingFrame = new KeyFrame(Duration.seconds(5.6),
|
||||
private void addKeyFrames(List<KeyFrame> frames, double angle, double duration) {
|
||||
frames.add(new KeyFrame(Duration.seconds(duration),
|
||||
new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
|
||||
new KeyValue(arc.startAngleProperty(),
|
||||
1845 + control.getStartingAngle(),
|
||||
Interpolator.LINEAR));
|
||||
angle + 45 + startingAngle,
|
||||
Interpolator.LINEAR)));
|
||||
frames.add(new KeyFrame(Duration.seconds(duration + 0.4),
|
||||
new KeyValue(arc.lengthProperty(), 250, Interpolator.LINEAR),
|
||||
new KeyValue(arc.startAngleProperty(),
|
||||
angle + 90 + startingAngle,
|
||||
Interpolator.LINEAR)));
|
||||
frames.add(new KeyFrame(Duration.seconds(duration + 0.7),
|
||||
new KeyValue(arc.lengthProperty(), 250, Interpolator.LINEAR),
|
||||
new KeyValue(arc.startAngleProperty(),
|
||||
angle + 135 + startingAngle,
|
||||
Interpolator.LINEAR)));
|
||||
frames.add(new KeyFrame(Duration.seconds(duration + 1.1),
|
||||
new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
|
||||
new KeyValue(arc.startAngleProperty(),
|
||||
angle + 435 + startingAngle,
|
||||
Interpolator.LINEAR)));
|
||||
}
|
||||
|
||||
if (timeline != null) {
|
||||
timeline.stop();
|
||||
timeline.getKeyFrames().clear();
|
||||
private Timeline createTransition() {
|
||||
Timeline timeline;
|
||||
if (AnimationUtils.isAnimationEnabled()) {
|
||||
var keyFrames = new ArrayList<KeyFrame>(17);
|
||||
addKeyFrames(keyFrames, 0, 0);
|
||||
addKeyFrames(keyFrames, 450, 1.4);
|
||||
addKeyFrames(keyFrames, 900, 2.8);
|
||||
addKeyFrames(keyFrames, 1350, 4.2);
|
||||
keyFrames.add(new KeyFrame(Duration.seconds(5.6),
|
||||
new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR),
|
||||
new KeyValue(arc.startAngleProperty(),
|
||||
1845 + startingAngle,
|
||||
Interpolator.LINEAR)));
|
||||
|
||||
timeline = new Timeline();
|
||||
timeline.getKeyFrames().setAll(keyFrames);
|
||||
} else {
|
||||
final double arcLength = 250;
|
||||
timeline = new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(arc.startAngleProperty(), 45 + startingAngle, Interpolator.LINEAR),
|
||||
new KeyValue(arc.lengthProperty(), arcLength, Interpolator.DISCRETE)),
|
||||
new KeyFrame(Duration.seconds(1.2),
|
||||
new KeyValue(arc.startAngleProperty(), 45 + 360 + startingAngle, Interpolator.LINEAR),
|
||||
new KeyValue(arc.lengthProperty(), arcLength, Interpolator.DISCRETE))
|
||||
);
|
||||
}
|
||||
timeline = new Timeline(blueFrame[0],
|
||||
blueFrame[1],
|
||||
blueFrame[2],
|
||||
blueFrame[3],
|
||||
redFrame[0],
|
||||
redFrame[1],
|
||||
redFrame[2],
|
||||
redFrame[3],
|
||||
yellowFrame[0],
|
||||
yellowFrame[1],
|
||||
yellowFrame[2],
|
||||
yellowFrame[3],
|
||||
greenFrame[0],
|
||||
greenFrame[1],
|
||||
greenFrame[2],
|
||||
greenFrame[3],
|
||||
endingFrame);
|
||||
|
||||
timeline.setCycleCount(Timeline.INDEFINITE);
|
||||
timeline.setDelay(Duration.ZERO);
|
||||
timeline.playFromStart();
|
||||
return timeline;
|
||||
}
|
||||
|
||||
private void clearAnimation() {
|
||||
if (timeline != null) {
|
||||
timeline.stop();
|
||||
timeline.getKeyFrames().clear();
|
||||
timeline = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.countly;
|
||||
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
|
||||
@@ -387,7 +387,16 @@ public final class LauncherHelper {
|
||||
if (setting.getJavaVersionType() == JavaVersionType.VERSION) {
|
||||
try {
|
||||
int targetJavaVersionMajor = Integer.parseInt(setting.getJavaVersion());
|
||||
GameJavaVersion minimumJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
|
||||
GameJavaVersion minimumJavaVersion = null;
|
||||
if (gameVersion.compareTo("1.12.2") == 0) {
|
||||
Optional<String> cleanroomVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.CLEANROOM);
|
||||
if (cleanroomVersion.isPresent()) {
|
||||
minimumJavaVersion = GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion.get());
|
||||
}
|
||||
}
|
||||
|
||||
if (minimumJavaVersion == null)
|
||||
minimumJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
|
||||
|
||||
if (minimumJavaVersion != null && targetJavaVersionMajor < minimumJavaVersion.majorVersion()) {
|
||||
Controllers.dialog(
|
||||
@@ -402,8 +411,17 @@ public final class LauncherHelper {
|
||||
targetJavaVersion = GameJavaVersion.get(targetJavaVersionMajor);
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
} else
|
||||
targetJavaVersion = version.getJavaVersion();
|
||||
} else {
|
||||
if (gameVersion.compareTo("1.12.2") == 0) {
|
||||
Optional<String> cleanroomVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.CLEANROOM);
|
||||
if (cleanroomVersion.isPresent()) {
|
||||
targetJavaVersion = GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion.get());
|
||||
}
|
||||
}
|
||||
|
||||
if (targetJavaVersion == null)
|
||||
targetJavaVersion = version.getJavaVersion();
|
||||
}
|
||||
|
||||
if (targetJavaVersion != null && supportedVersions.contains(targetJavaVersion)) {
|
||||
downloadJava(targetJavaVersion, profile)
|
||||
@@ -433,7 +451,7 @@ public final class LauncherHelper {
|
||||
if (java != null) {
|
||||
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
|
||||
if (constraint.appliesToVersion(gameVersion, version, java, analyzer)) {
|
||||
if (!constraint.checkJava(gameVersion, version, java)) {
|
||||
if (!constraint.checkJava(gameVersion, version, java, analyzer)) {
|
||||
if (constraint.isMandatory()) {
|
||||
violatedMandatoryConstraints.add(constraint);
|
||||
} else {
|
||||
@@ -468,9 +486,14 @@ public final class LauncherHelper {
|
||||
return result;
|
||||
} else {
|
||||
GameJavaVersion gameJavaVersion;
|
||||
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.CLEANROOM_JAVA_21))
|
||||
gameJavaVersion = GameJavaVersion.JAVA_21;
|
||||
else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.GAME_JSON))
|
||||
if (violatedMandatoryConstraints.contains(JavaVersionConstraint.CLEANROOM)) {
|
||||
String cleanroomVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.CLEANROOM)
|
||||
.orElse("");
|
||||
|
||||
gameJavaVersion = !cleanroomVersion.isEmpty()
|
||||
? GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion)
|
||||
: GameJavaVersion.JAVA_21;
|
||||
} else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.GAME_JSON))
|
||||
gameJavaVersion = version.getJavaVersion();
|
||||
else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA))
|
||||
gameJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion);
|
||||
@@ -560,9 +583,14 @@ public final class LauncherHelper {
|
||||
case MODDED_JAVA_21:
|
||||
suggestions.add(i18n("launch.advice.modded_java", 21, gameVersion));
|
||||
break;
|
||||
case CLEANROOM_JAVA_21:
|
||||
suggestions.add(i18n("launch.advice.cleanroom"));
|
||||
case CLEANROOM: {
|
||||
String cleanroomVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.CLEANROOM).orElse("");
|
||||
if (!cleanroomVersion.isEmpty())
|
||||
suggestions.add(i18n("launch.advice.cleanroom", GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion).majorVersion(), cleanroomVersion));
|
||||
else
|
||||
suggestions.add(i18n("launch.advice.cleanroom", 21, ""));
|
||||
break;
|
||||
}
|
||||
case VANILLA_JAVA_8_51:
|
||||
suggestions.add(i18n("launch.advice.java8_51_1_13"));
|
||||
break;
|
||||
|
||||
@@ -115,8 +115,8 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
|
||||
}
|
||||
|
||||
@Override
|
||||
public RemoteMod getModById(String id) throws IOException {
|
||||
return getBackedRemoteModRepository().getModById(id);
|
||||
public RemoteMod getModById(DownloadProvider downloadProvider, String id) throws IOException {
|
||||
return getBackedRemoteModRepository().getModById(downloadProvider, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -125,7 +125,7 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
|
||||
return getBackedRemoteModRepository().getRemoteVersionsById(id);
|
||||
public Stream<RemoteMod.Version> getRemoteVersionsById(DownloadProvider downloadProvider, String id) throws IOException {
|
||||
return getBackedRemoteModRepository().getRemoteVersionsById(downloadProvider, id);
|
||||
}
|
||||
}
|
||||
@@ -304,7 +304,7 @@ public final class JavaManager {
|
||||
|
||||
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
|
||||
if (constraint.appliesToVersion(gameVersion, version, java, analyzer)) {
|
||||
if (!constraint.checkJava(gameVersion, version, java)) {
|
||||
if (!constraint.checkJava(gameVersion, version, java, analyzer)) {
|
||||
if (constraint.isMandatory()) {
|
||||
violationMandatory = true;
|
||||
} else {
|
||||
|
||||
@@ -237,6 +237,21 @@ public final class Config extends ObservableSetting {
|
||||
this.disableAutoShowUpdateDialog.set(disableAutoShowUpdateDialog);
|
||||
}
|
||||
|
||||
@SerializedName("disableAprilFools")
|
||||
private final BooleanProperty disableAprilFools = new SimpleBooleanProperty(false);
|
||||
|
||||
public BooleanProperty disableAprilFoolsProperty() {
|
||||
return disableAprilFools;
|
||||
}
|
||||
|
||||
public boolean isDisableAprilFools() {
|
||||
return disableAprilFools.get();
|
||||
}
|
||||
|
||||
public void setDisableAprilFools(boolean disableAprilFools) {
|
||||
this.disableAprilFools.set(disableAprilFools);
|
||||
}
|
||||
|
||||
@SerializedName("shownTips")
|
||||
private final ObservableMap<String, Object> shownTips = FXCollections.observableHashMap();
|
||||
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
public final class SambaException extends RuntimeException {
|
||||
|
||||
@@ -23,6 +23,7 @@ import com.jfoenix.validation.base.ValidatorBase;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
@@ -64,12 +65,16 @@ import org.jackhuang.hmcl.ui.versions.GameListPage;
|
||||
import org.jackhuang.hmcl.ui.versions.VersionPage;
|
||||
import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
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.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@@ -82,6 +87,7 @@ public final class Controllers {
|
||||
public static final String JAVA_VERSION_TIP = "javaVersion";
|
||||
public static final String JAVA_INTERPRETED_MODE_TIP = "javaInterpretedMode";
|
||||
public static final String SOFTWARE_RENDERING = "softwareRendering";
|
||||
public static final String APRIL_FOOLS = "aprilFools";
|
||||
|
||||
public static final int MIN_WIDTH = 800 + 2 + 16; // bg width + border width*2 + shadow width*2
|
||||
public static final int MIN_HEIGHT = 450 + 2 + 40 + 16; // bg height + border width*2 + toolbar height + shadow width*2
|
||||
@@ -225,6 +231,8 @@ public final class Controllers {
|
||||
public static void initialize(Stage stage) {
|
||||
LOG.info("Start initializing application");
|
||||
|
||||
LOG.info("April Fools: " + AprilFools.isEnabled());
|
||||
|
||||
if (System.getProperty("prism.lcdtext") == null) {
|
||||
String fontAntiAliasing = globalConfig().getFontAntiAliasing();
|
||||
if ("lcd".equalsIgnoreCase(fontAntiAliasing)) {
|
||||
@@ -432,6 +440,53 @@ public final class Controllers {
|
||||
agreementPane.setActions(agreementLink, yesButton, noButton);
|
||||
Controllers.dialog(agreementPane);
|
||||
}
|
||||
|
||||
aprilFools:
|
||||
if (AprilFools.isEnabled()) {
|
||||
int currentYear = LocalDate.now().getYear();
|
||||
if (config().getShownTips().get(APRIL_FOOLS) instanceof Number year && year.intValue() >= currentYear)
|
||||
break aprilFools;
|
||||
|
||||
if (!I18n.getLocale().getLocale().getLanguage().equals("zh"))
|
||||
break aprilFools;
|
||||
|
||||
SupportedLocale lzh = SupportedLocale.getSupportedLocales().stream()
|
||||
.filter(locale -> "lzh".equals(locale.getName()))
|
||||
.findFirst().orElse(null);
|
||||
|
||||
if (lzh == null) {
|
||||
LOG.warning("No supported locale found for lzh");
|
||||
break aprilFools;
|
||||
}
|
||||
|
||||
Runnable updateShowTips = () -> config().getShownTips().put(APRIL_FOOLS, currentYear);
|
||||
|
||||
Controllers.confirmWithCountdown(i18n("launcher.april_fools.switch_lzh"), null, 10,
|
||||
MessageType.QUESTION, () -> {
|
||||
Controllers.confirm(i18n("launcher.april_fools.switch_lzh.confirm"), null, MessageType.QUESTION, () -> {
|
||||
LOG.info("Switching locale to " + lzh);
|
||||
|
||||
updateShowTips.run();
|
||||
config().setLocalization(lzh);
|
||||
|
||||
Controllers.onApplicationStop();
|
||||
|
||||
try {
|
||||
FileSaver.waitForAllSaves();
|
||||
} catch (InterruptedException ignored) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
try {
|
||||
Restarter.restartSelf();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to restart self", e);
|
||||
}
|
||||
|
||||
Platform.exit();
|
||||
}, updateShowTips);
|
||||
}, updateShowTips);
|
||||
}
|
||||
}
|
||||
|
||||
public static void dialog(Region content) {
|
||||
|
||||
@@ -1241,8 +1241,8 @@ public final class FXUtils {
|
||||
.setSignificance(Task.TaskSignificance.MINOR);
|
||||
}
|
||||
|
||||
public static Task<Image> getRemoteImageTask(URI uri, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
|
||||
return new CacheFileTask(uri)
|
||||
public static Task<Image> getRemoteImageTask(List<URI> uris, int requestedWidth, int requestedHeight, boolean preserveRatio, boolean smooth) {
|
||||
return new CacheFileTask(uris)
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.thenApplyAsync(file -> loadImage(file, requestedWidth, requestedHeight, preserveRatio, smooth))
|
||||
.setSignificance(Task.TaskSignificance.MINOR);
|
||||
|
||||
@@ -42,6 +42,7 @@ import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
@@ -273,10 +274,10 @@ public class GameCrashWindow extends Stage {
|
||||
logWindow.show();
|
||||
}
|
||||
|
||||
private void exportGameCrashInfo() {
|
||||
private CompletableFuture<Path> exportGameCrashInfo() {
|
||||
Path logFile = Paths.get("minecraft-exported-crash-info-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".zip").toAbsolutePath();
|
||||
|
||||
CompletableFuture.supplyAsync(() ->
|
||||
return CompletableFuture.supplyAsync(() ->
|
||||
logs.stream().map(Log::getLog).collect(Collectors.joining("\n")))
|
||||
.thenComposeAsync(logs -> {
|
||||
long processStartTime = managedProcess.getProcess().info()
|
||||
@@ -301,20 +302,7 @@ public class GameCrashWindow extends Stage {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
})
|
||||
.handleAsync((result, exception) -> {
|
||||
if (exception == null) {
|
||||
FXUtils.showFileInExplorer(logFile);
|
||||
var dialog = new MessageDialogPane.Builder(i18n("settings.launcher.launcher_log.export.success", logFile), i18n("message.success"), MessageDialogPane.MessageType.SUCCESS).ok(null).build();
|
||||
DialogUtils.show(stackPane, dialog);
|
||||
} else {
|
||||
LOG.warning("Failed to export game crash info", exception);
|
||||
var dialog = new MessageDialogPane.Builder(i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception), i18n("message.error"), MessageDialogPane.MessageType.ERROR).ok(null).build();
|
||||
DialogUtils.show(stackPane, dialog);
|
||||
}
|
||||
|
||||
return null;
|
||||
}, Schedulers.javafx());
|
||||
}).thenApply(ignored -> logFile);
|
||||
}
|
||||
|
||||
private final class View extends VBox {
|
||||
@@ -444,8 +432,35 @@ public class GameCrashWindow extends Stage {
|
||||
HBox toolBar = new HBox();
|
||||
VBox.setMargin(toolBar, new Insets(0, 0, 4, 0));
|
||||
{
|
||||
JFXButton exportGameCrashInfoButton = FXUtils.newRaisedButton(i18n("logwindow.export_game_crash_logs"));
|
||||
exportGameCrashInfoButton.setOnAction(e -> exportGameCrashInfo());
|
||||
SpinnerPane exportButtonPane = new SpinnerPane();
|
||||
exportButtonPane.getStyleClass().add("small-spinner-pane");
|
||||
|
||||
JFXButton exportButton = FXUtils.newRaisedButton(i18n("logwindow.export_game_crash_logs"));
|
||||
exportButtonPane.setContent(exportButton);
|
||||
exportButton.setOnAction(e -> {
|
||||
exportButtonPane.showSpinner();
|
||||
exportGameCrashInfo().whenCompleteAsync((result, exception) -> {
|
||||
exportButtonPane.hideSpinner();
|
||||
|
||||
if (exception == null) {
|
||||
FXUtils.showFileInExplorer(result);
|
||||
var dialog = new MessageDialogPane.Builder(
|
||||
i18n("settings.launcher.launcher_log.export.success", result),
|
||||
i18n("message.success"),
|
||||
MessageDialogPane.MessageType.SUCCESS
|
||||
).ok(null).build();
|
||||
DialogUtils.show(stackPane, dialog);
|
||||
} else {
|
||||
LOG.warning("Failed to export game crash info", exception);
|
||||
var dialog = new MessageDialogPane.Builder(
|
||||
i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception),
|
||||
i18n("message.error"),
|
||||
MessageDialogPane.MessageType.ERROR
|
||||
).ok(null).build();
|
||||
DialogUtils.show(stackPane, dialog);
|
||||
}
|
||||
}, Schedulers.javafx());
|
||||
});
|
||||
|
||||
JFXButton logButton = FXUtils.newRaisedButton(i18n("logwindow.title"));
|
||||
logButton.setOnAction(e -> showLogWindow());
|
||||
@@ -457,7 +472,7 @@ public class GameCrashWindow extends Stage {
|
||||
toolBar.setPadding(new Insets(8));
|
||||
toolBar.setSpacing(8);
|
||||
toolBar.getStyleClass().add("jfx-tool-bar");
|
||||
toolBar.getChildren().setAll(exportGameCrashInfoButton, logButton, helpButton);
|
||||
toolBar.getChildren().setAll(exportButtonPane, logButton, helpButton);
|
||||
}
|
||||
|
||||
getChildren().setAll(titlePane, infoPane, moddedPane, gameDirPane, toolBar);
|
||||
|
||||
@@ -36,6 +36,7 @@ import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.game.GameDumpGenerator;
|
||||
import org.jackhuang.hmcl.game.Log;
|
||||
import org.jackhuang.hmcl.setting.StyleSheets;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
|
||||
@@ -241,6 +242,10 @@ public final class LogWindow extends Stage {
|
||||
});
|
||||
}
|
||||
|
||||
private ManagedProcess getGameProcess() {
|
||||
return gameProcess;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new LogWindowSkin(this);
|
||||
@@ -425,6 +430,13 @@ public final class LogWindow extends Stage {
|
||||
clearButton.setOnAction(e -> getSkinnable().onClear());
|
||||
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, exportDumpPane, clearButton);
|
||||
|
||||
control.getGameProcess().getProcess()
|
||||
.onExit()
|
||||
.thenRunAsync(() -> {
|
||||
terminateButton.setDisable(true);
|
||||
exportDumpButton.setDisable(true);
|
||||
}, Schedulers.javafx());
|
||||
|
||||
vbox.getChildren().add(bottom);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import org.jackhuang.hmcl.util.FXThread;
|
||||
import org.jackhuang.hmcl.util.MathUtils;
|
||||
import org.jackhuang.hmcl.util.platform.SystemInfo;
|
||||
import org.jackhuang.hmcl.util.platform.hardware.PhysicalMemoryStatus;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/// @author Glavo
|
||||
public final class MemoryStatusBar extends Control {
|
||||
|
||||
private static final String DEFAULT_STYLE_CLASS = "memory-status-bar";
|
||||
|
||||
public MemoryStatusBar() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
|
||||
setPrefWidth(200);
|
||||
}
|
||||
|
||||
private final ReadOnlyObjectProperty<PhysicalMemoryStatus> memoryStatus = UpdateMemoryStatus.memoryStatusProperty();
|
||||
|
||||
/// The memory status of the system.
|
||||
///
|
||||
/// The property will be automatically updated.
|
||||
///
|
||||
/// The property has no bean, because it may share between multiple MemoryStatusBar instances.
|
||||
public ReadOnlyObjectProperty<PhysicalMemoryStatus> memoryStatusProperty() {
|
||||
return memoryStatus;
|
||||
}
|
||||
|
||||
public PhysicalMemoryStatus getMemoryStatus() {
|
||||
return memoryStatus.get();
|
||||
}
|
||||
|
||||
private final DoubleProperty memoryAllocated = new SimpleDoubleProperty();
|
||||
|
||||
public DoubleProperty memoryAllocatedProperty() {
|
||||
return memoryAllocated;
|
||||
}
|
||||
|
||||
public double getMemoryAllocated() {
|
||||
return memoryAllocated.get();
|
||||
}
|
||||
|
||||
public void setMemoryAllocated(double memoryAllocated) {
|
||||
this.memoryAllocated.set(memoryAllocated);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SkinBase<MemoryStatusBar> createDefaultSkin() {
|
||||
return new Skin(this);
|
||||
}
|
||||
|
||||
private static final class Skin extends SkinBase<MemoryStatusBar> {
|
||||
private static final int HEIGHT = 4;
|
||||
|
||||
private final StackPane track;
|
||||
private final Rectangle memoryUsed;
|
||||
private final Rectangle memoryAllocate;
|
||||
|
||||
private double contentWidth = -1;
|
||||
|
||||
Skin(MemoryStatusBar control) {
|
||||
super(control);
|
||||
|
||||
track = new StackPane();
|
||||
track.getStyleClass().add("track");
|
||||
track.setPadding(new Insets(HEIGHT, 0, 0, 0));
|
||||
|
||||
memoryUsed = new Rectangle();
|
||||
memoryUsed.setManaged(false);
|
||||
memoryUsed.setArcWidth(HEIGHT);
|
||||
memoryUsed.setArcHeight(HEIGHT);
|
||||
memoryUsed.getStyleClass().add("memory-used");
|
||||
|
||||
memoryAllocate = new Rectangle();
|
||||
memoryAllocate.setManaged(false);
|
||||
memoryAllocate.setArcWidth(HEIGHT);
|
||||
memoryAllocate.setArcHeight(HEIGHT);
|
||||
memoryAllocate.getStyleClass().add("memory-allocate");
|
||||
|
||||
this.getChildren().setAll(track, memoryAllocate, memoryUsed);
|
||||
|
||||
registerInvalidationListener(control.memoryStatusProperty(), it -> updateBar());
|
||||
registerInvalidationListener(control.memoryAllocatedProperty(), it -> updateBar());
|
||||
}
|
||||
|
||||
private void updateBar() {
|
||||
if (contentWidth <= 0) {
|
||||
// not yet layout
|
||||
return;
|
||||
}
|
||||
|
||||
PhysicalMemoryStatus status = getSkinnable().getMemoryStatus();
|
||||
double total = status.getTotal();
|
||||
double used = status.getUsed();
|
||||
double allocated = getSkinnable().getMemoryAllocated();
|
||||
|
||||
if (total <= 0) {
|
||||
memoryUsed.setWidth(contentWidth);
|
||||
memoryAllocate.setWidth(contentWidth);
|
||||
return;
|
||||
}
|
||||
|
||||
double usedWidth = MathUtils.clamp(contentWidth * (used / total), 0, contentWidth);
|
||||
double allocatedWidth = MathUtils.clamp(contentWidth * (allocated / total) + usedWidth, 0, contentWidth);
|
||||
|
||||
memoryUsed.setWidth(usedWidth);
|
||||
memoryAllocate.setWidth(allocatedWidth);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
|
||||
track.resizeRelocate(contentX, contentY, contentWidth, contentHeight);
|
||||
memoryUsed.relocate(contentX, contentY);
|
||||
memoryAllocate.relocate(contentX, contentY);
|
||||
|
||||
this.contentWidth = contentWidth;
|
||||
|
||||
memoryUsed.setHeight(contentHeight);
|
||||
memoryAllocate.setHeight(contentHeight);
|
||||
|
||||
updateBar();
|
||||
}
|
||||
|
||||
@Override
|
||||
public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
return Node.BASELINE_OFFSET_SAME_AS_HEIGHT;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
return leftInset + track.prefWidth(height) + rightInset;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
return topInset + HEIGHT + bottomInset;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
|
||||
return computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class UpdateMemoryStatus extends Thread {
|
||||
|
||||
private static final int UPDATE_INTERVAL = 3000;
|
||||
|
||||
@FXThread
|
||||
private static WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyCache;
|
||||
|
||||
@FXThread
|
||||
static ReadOnlyObjectProperty<PhysicalMemoryStatus> memoryStatusProperty() {
|
||||
if (memoryStatusPropertyCache != null) {
|
||||
var property = memoryStatusPropertyCache.get();
|
||||
if (property != null) {
|
||||
return property;
|
||||
}
|
||||
}
|
||||
|
||||
ObjectProperty<PhysicalMemoryStatus> property = new SimpleObjectProperty<>(PhysicalMemoryStatus.INVALID);
|
||||
memoryStatusPropertyCache = new WeakReference<>(property);
|
||||
new UpdateMemoryStatus(memoryStatusPropertyCache).start();
|
||||
return property;
|
||||
}
|
||||
|
||||
private final WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyRef;
|
||||
|
||||
UpdateMemoryStatus(WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyRef) {
|
||||
this.memoryStatusPropertyRef = memoryStatusPropertyRef;
|
||||
|
||||
setName("UpdateMemoryStatus");
|
||||
setDaemon(true);
|
||||
setPriority(Thread.MIN_PRIORITY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while (true) {
|
||||
PhysicalMemoryStatus status = SystemInfo.getPhysicalMemoryStatus();
|
||||
|
||||
var memoryStatusProperty = memoryStatusPropertyRef.get();
|
||||
if (memoryStatusProperty == null)
|
||||
return;
|
||||
|
||||
if (Controllers.isStopped())
|
||||
return;
|
||||
|
||||
Platform.runLater(() -> memoryStatusProperty.set(status));
|
||||
|
||||
try {
|
||||
//noinspection BusyWait
|
||||
Thread.sleep(UPDATE_INTERVAL);
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ import javafx.scene.control.ScrollBar;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.MathUtils;
|
||||
|
||||
// Referenced in root.css
|
||||
@SuppressWarnings("unused")
|
||||
@@ -75,7 +75,7 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
|
||||
*/
|
||||
if (getSkinnable().getMax() > getSkinnable().getMin()) {
|
||||
dragStart = thumb.localToParent(me.getX(), me.getY());
|
||||
double clampedValue = Lang.clamp(getSkinnable().getMin(), getSkinnable().getValue(), getSkinnable().getMax());
|
||||
double clampedValue = MathUtils.clamp(getSkinnable().getValue(), getSkinnable().getMin(), getSkinnable().getMax());
|
||||
preDragThumbPos = (clampedValue - getSkinnable().getMin()) / (getSkinnable().getMax() - getSkinnable().getMin());
|
||||
me.consume();
|
||||
}
|
||||
@@ -107,7 +107,7 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
|
||||
getSkinnable().requestFocus();
|
||||
double newValue = (position * (getSkinnable().getMax() - getSkinnable().getMin())) + getSkinnable().getMin();
|
||||
if (!Double.isNaN(newValue)) {
|
||||
getSkinnable().setValue(Lang.clamp(getSkinnable().getMin(), newValue, getSkinnable().getMax()));
|
||||
getSkinnable().setValue(MathUtils.clamp(newValue, getSkinnable().getMin(), getSkinnable().getMax()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -83,8 +83,8 @@ import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class TaskListPane extends StackPane {
|
||||
private static final Insets DEFAULT_PROGRESS_NODE_PADDING = new Insets(0, 0, 8, 0);
|
||||
private static final Insets STAGED_PROGRESS_NODE_PADDING = new Insets(0, 0, 8, 26);
|
||||
private static final Insets DEFAULT_PROGRESS_NODE_PADDING = new Insets(0, 0, 4, 0);
|
||||
private static final Insets STAGED_PROGRESS_NODE_PADDING = new Insets(0, 0, 4, 26);
|
||||
|
||||
private TaskExecutor executor;
|
||||
private final JFXListView<Node> listView = new JFXListView<>();
|
||||
@@ -279,6 +279,7 @@ public final class TaskListPane extends StackPane {
|
||||
|
||||
private final class Cell extends ListCell<Node> {
|
||||
private static final double STATUS_ICON_SIZE = 14;
|
||||
private static final Insets PROGRESS_BAR_MARGIN = new Insets(2, 0, 0, 0);
|
||||
|
||||
private final BorderPane pane = new BorderPane();
|
||||
private final StackPane left = new StackPane();
|
||||
@@ -310,6 +311,7 @@ public final class TaskListPane extends StackPane {
|
||||
bar.minWidthProperty().bind(barWidth);
|
||||
bar.prefWidthProperty().bind(barWidth);
|
||||
bar.maxWidthProperty().bind(barWidth);
|
||||
BorderPane.setMargin(bar, PROGRESS_BAR_MARGIN);
|
||||
|
||||
setGraphic(pane);
|
||||
}
|
||||
@@ -325,8 +327,9 @@ public final class TaskListPane extends StackPane {
|
||||
pane.paddingProperty().unbind();
|
||||
title.textProperty().unbind();
|
||||
message.textProperty().unbind();
|
||||
bar.progressProperty().unbind();
|
||||
|
||||
bar.setSmoothProgress(false);
|
||||
bar.progressProperty().unbind();
|
||||
StageNode prevStageNode;
|
||||
if (prevStageNodeRef != null && (prevStageNode = prevStageNodeRef.get()) != null)
|
||||
prevStageNode.status.removeListener(statusChangeListener);
|
||||
@@ -364,6 +367,8 @@ public final class TaskListPane extends StackPane {
|
||||
pane.setRight(null);
|
||||
pane.setBottom(null);
|
||||
}
|
||||
|
||||
bar.setSmoothProgress(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.property.StringPropertyBase;
|
||||
@@ -168,11 +169,13 @@ public class TwoLineListItem extends VBox {
|
||||
|
||||
var scrollPane = new ScrollPane(tagsBox);
|
||||
HBox.setHgrow(scrollPane, Priority.ALWAYS);
|
||||
lblTitle.setMinWidth(Label.USE_PREF_SIZE);
|
||||
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
FXUtils.onChangeAndOperate(tagsBox.heightProperty(), height -> FXUtils.setLimitHeight(scrollPane, height.doubleValue()));
|
||||
scrollPane.setPrefWidth(50);
|
||||
firstLine.getChildren().setAll(lblTitle, scrollPane);
|
||||
|
||||
tags.addListener((InvalidationListener) ignored -> scrollPane.requestLayout());
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ import org.jackhuang.hmcl.ui.construct.JFXDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.Navigator;
|
||||
import org.jackhuang.hmcl.ui.wizard.Refreshable;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.MathUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -218,7 +218,7 @@ public class DecoratorController {
|
||||
return new Background(new BackgroundFill(new Color(1, 1, 1, 0.5), CornerRadii.EMPTY, Insets.EMPTY));
|
||||
case PAINT:
|
||||
Paint paint = config().getBackgroundPaint();
|
||||
double opacity = Lang.clamp(0, config().getBackgroundImageOpacity(), 100) / 100.;
|
||||
double opacity = MathUtils.clamp(config().getBackgroundImageOpacity(), 0, 100) / 100.;
|
||||
if (paint instanceof Color || paint == null) {
|
||||
Color color = (Color) paint;
|
||||
if (color == null)
|
||||
|
||||
@@ -84,8 +84,8 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
newGameTab.setNodeSupplier(loadVersionFor(() -> new VersionsPage(versionPageNavigator, i18n("install.installer.choose", i18n("install.installer.game")), "", DownloadProviders.getDownloadProvider(),
|
||||
"game", versionPageNavigator::onGameSelected)));
|
||||
modpackTab.setNodeSupplier(loadVersionFor(() -> {
|
||||
DownloadListPage page = HMCLLocalizedDownloadListPage.ofModPack((profile, __, mod, file) -> {
|
||||
Versions.downloadModpackImpl(profile, uploadVersion, mod, file);
|
||||
DownloadListPage page = HMCLLocalizedDownloadListPage.ofModPack((downloadProvider, profile, __, mod, file) -> {
|
||||
Versions.downloadModpackImpl(downloadProvider, profile, uploadVersion, mod, file);
|
||||
}, false);
|
||||
|
||||
JFXButton installLocalModpackButton = FXUtils.newRaisedButton(i18n("install.modpack"));
|
||||
@@ -94,9 +94,9 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
page.getActions().add(installLocalModpackButton);
|
||||
return page;
|
||||
}));
|
||||
modTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofMod((profile, version, mod, file) -> download(profile, version, file, "mods"), true)));
|
||||
resourcePackTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofResourcePack((profile, version, mod, file) -> download(profile, version, file, "resourcepacks"), true)));
|
||||
shaderTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofShaderPack((profile, version, mod, file) -> download(profile, version, file, "shaderpacks"), true)));
|
||||
modTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofMod((downloadProvider, profile, version, mod, file) -> download(downloadProvider, profile, version, file, "mods"), true)));
|
||||
resourcePackTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofResourcePack((downloadProvider, profile, version, mod, file) -> download(downloadProvider, profile, version, file, "resourcepacks"), true)));
|
||||
shaderTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofShaderPack((downloadProvider, profile, version, mod, file) -> download(downloadProvider, profile, version, file, "shaderpacks"), true)));
|
||||
worldTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteModRepository.WORLDS)));
|
||||
tab = new TabHeader(transitionPane, newGameTab, modpackTab, modTab, resourcePackTab, shaderTab, worldTab);
|
||||
|
||||
@@ -129,7 +129,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
};
|
||||
}
|
||||
|
||||
public static void download(Profile profile, @Nullable String version, RemoteMod.Version file, String subdirectoryName) {
|
||||
public static void download(DownloadProvider downloadProvider, Profile profile, @Nullable String version, RemoteMod.Version file, String subdirectoryName) {
|
||||
if (version == null) version = profile.getSelectedVersion();
|
||||
|
||||
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version) : profile.getRepository().getBaseDirectory();
|
||||
@@ -138,7 +138,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
Path dest = runDirectory.resolve(subdirectoryName).resolve(result);
|
||||
|
||||
Controllers.taskDialog(Task.composeAsync(() -> {
|
||||
var task = new FileDownloadTask(file.getFile().getUrl(), dest);
|
||||
var task = new FileDownloadTask(downloadProvider.injectURLWithCandidates(file.getFile().getUrl()), dest);
|
||||
task.setName(file.getName());
|
||||
return task;
|
||||
}).whenComplete(Schedulers.javafx(), exception -> {
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
|
||||
@@ -19,7 +19,6 @@ package org.jackhuang.hmcl.ui.main;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXRadioButton;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
@@ -36,6 +35,7 @@ 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.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
@@ -45,6 +45,8 @@ 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.AprilFools;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
|
||||
@@ -60,12 +62,12 @@ import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
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;
|
||||
@@ -224,6 +226,14 @@ public final class SettingsPage extends ScrollPane {
|
||||
settingsPane.getContent().add(disableAutoShowUpdateDialogPane);
|
||||
}
|
||||
|
||||
if (AprilFools.isShowAprilFoolsSettings()) {
|
||||
LineToggleButton disableAprilFools = new LineToggleButton();
|
||||
disableAprilFools.setTitle(i18n("settings.launcher.disable_april_fools"));
|
||||
disableAprilFools.setSubtitle(i18n("settings.take_effect_after_restart"));
|
||||
disableAprilFools.selectedProperty().bindBidirectional(config().disableAprilFoolsProperty());
|
||||
settingsPane.getContent().add(disableAprilFools);
|
||||
}
|
||||
|
||||
{
|
||||
MultiFileItem<EnumCommonDirectory> fileCommonLocation = new MultiFileItem<>();
|
||||
fileCommonLocation.loadChildren(Arrays.asList(
|
||||
@@ -292,12 +302,31 @@ public final class SettingsPage extends ScrollPane {
|
||||
if (LOG.getLogFile() == null)
|
||||
openLogFolderButton.setDisable(true);
|
||||
|
||||
SpinnerPane exportLogPane = new SpinnerPane();
|
||||
|
||||
JFXButton logButton = FXUtils.newBorderButton(i18n("settings.launcher.launcher_log.export"));
|
||||
logButton.setOnAction(e -> onExportLogs());
|
||||
exportLogPane.setContent(logButton);
|
||||
logButton.setOnAction(e -> {
|
||||
exportLogPane.showSpinner();
|
||||
onExportLogs().whenCompleteAsync((result, exception) -> {
|
||||
exportLogPane.hideSpinner();
|
||||
if (exception == null) {
|
||||
Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", result));
|
||||
FXUtils.showFileInExplorer(result);
|
||||
} else {
|
||||
LOG.warning("Failed to export logs", exception);
|
||||
Controllers.dialog(
|
||||
i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception),
|
||||
null,
|
||||
MessageType.ERROR
|
||||
);
|
||||
}
|
||||
}, Schedulers.javafx());
|
||||
});
|
||||
|
||||
HBox buttonBox = new HBox();
|
||||
buttonBox.setSpacing(10);
|
||||
buttonBox.getChildren().addAll(openLogFolderButton, logButton);
|
||||
buttonBox.getChildren().addAll(openLogFolderButton, exportLogPane);
|
||||
BorderPane.setAlignment(buttonBox, Pos.CENTER_RIGHT);
|
||||
debugPane.setRight(buttonBox);
|
||||
|
||||
@@ -369,86 +398,79 @@ public final class SettingsPage extends ScrollPane {
|
||||
}
|
||||
}
|
||||
|
||||
private void onExportLogs() {
|
||||
thread(() -> {
|
||||
private CompletableFuture<Path> onExportLogs() {
|
||||
return CompletableFuture.supplyAsync(Lang.wrap(() -> {
|
||||
String nameBase = "hmcl-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss"));
|
||||
List<Path> recentLogFiles = LOG.findRecentLogFiles(5);
|
||||
|
||||
Path outputFile;
|
||||
try {
|
||||
if (recentLogFiles.isEmpty()) {
|
||||
outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".log");
|
||||
if (recentLogFiles.isEmpty()) {
|
||||
outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".log");
|
||||
|
||||
LOG.info("Exporting latest logs to " + outputFile);
|
||||
try (OutputStream output = Files.newOutputStream(outputFile)) {
|
||||
LOG.exportLogs(output);
|
||||
}
|
||||
} else {
|
||||
outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".zip");
|
||||
LOG.info("Exporting latest logs to " + outputFile);
|
||||
try (OutputStream output = Files.newOutputStream(outputFile)) {
|
||||
LOG.exportLogs(output);
|
||||
}
|
||||
} else {
|
||||
outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".zip");
|
||||
|
||||
LOG.info("Exporting latest logs to " + outputFile);
|
||||
LOG.info("Exporting latest logs to " + outputFile);
|
||||
|
||||
byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
|
||||
try (var os = Files.newOutputStream(outputFile);
|
||||
var zos = new ZipOutputStream(os)) {
|
||||
byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
|
||||
try (var os = Files.newOutputStream(outputFile);
|
||||
var zos = new ZipOutputStream(os)) {
|
||||
|
||||
Set<String> entryNames = new HashSet<>();
|
||||
Set<String> entryNames = new HashSet<>();
|
||||
|
||||
for (Path path : recentLogFiles) {
|
||||
String fileName = FileUtils.getName(path);
|
||||
String extension = StringUtils.substringAfterLast(fileName, '.');
|
||||
for (Path path : recentLogFiles) {
|
||||
String fileName = FileUtils.getName(path);
|
||||
String extension = StringUtils.substringAfterLast(fileName, '.');
|
||||
|
||||
if ("gz".equals(extension) || "xz".equals(extension)) {
|
||||
// If an exception occurs while decompressing the input file, we should
|
||||
// ensure the input file and the current zip entry are closed,
|
||||
// then copy the compressed file content as-is into a new entry in the zip file.
|
||||
|
||||
InputStream input = null;
|
||||
try {
|
||||
input = Files.newInputStream(path);
|
||||
input = "gz".equals(extension)
|
||||
? new GZIPInputStream(input)
|
||||
: new XZInputStream(input);
|
||||
} catch (Throwable ex) {
|
||||
LOG.warning("Failed to open log file " + path, ex);
|
||||
IOUtils.closeQuietly(input, ex);
|
||||
input = null;
|
||||
}
|
||||
|
||||
String entryName = getEntryName(entryNames, StringUtils.substringBeforeLast(fileName, "."));
|
||||
if (input != null && exportLogFile(zos, path, entryName, input, buffer))
|
||||
continue;
|
||||
}
|
||||
|
||||
// Copy the log file content as-is into a new entry in the zip file.
|
||||
if ("gz".equals(extension) || "xz".equals(extension)) {
|
||||
// If an exception occurs while decompressing the input file, we should
|
||||
// ensure the input file and the current zip entry are closed.
|
||||
// ensure the input file and the current zip entry are closed,
|
||||
// then copy the compressed file content as-is into a new entry in the zip file.
|
||||
|
||||
InputStream input;
|
||||
InputStream input = null;
|
||||
try {
|
||||
input = Files.newInputStream(path);
|
||||
input = "gz".equals(extension)
|
||||
? new GZIPInputStream(input)
|
||||
: new XZInputStream(input);
|
||||
} catch (Throwable ex) {
|
||||
LOG.warning("Failed to open log file " + path, ex);
|
||||
continue;
|
||||
IOUtils.closeQuietly(input, ex);
|
||||
input = null;
|
||||
}
|
||||
|
||||
exportLogFile(zos, path, getEntryName(entryNames, fileName), input, buffer);
|
||||
String entryName = getEntryName(entryNames, StringUtils.substringBeforeLast(fileName, "."));
|
||||
if (input != null && exportLogFile(zos, path, entryName, input, buffer))
|
||||
continue;
|
||||
}
|
||||
|
||||
zos.putNextEntry(new ZipEntry(getEntryName(entryNames, "hmcl-latest.log")));
|
||||
LOG.exportLogs(zos);
|
||||
zos.closeEntry();
|
||||
// Copy the log file content as-is into a new entry in the zip file.
|
||||
// If an exception occurs while decompressing the input file, we should
|
||||
// ensure the input file and the current zip entry are closed.
|
||||
|
||||
InputStream input;
|
||||
try {
|
||||
input = Files.newInputStream(path);
|
||||
} catch (Throwable ex) {
|
||||
LOG.warning("Failed to open log file " + path, ex);
|
||||
continue;
|
||||
}
|
||||
|
||||
exportLogFile(zos, path, getEntryName(entryNames, fileName), input, buffer);
|
||||
}
|
||||
|
||||
zos.putNextEntry(new ZipEntry(getEntryName(entryNames, "hmcl-latest.log")));
|
||||
LOG.exportLogs(zos);
|
||||
zos.closeEntry();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to export logs", e);
|
||||
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(e), null, MessageType.ERROR));
|
||||
return;
|
||||
}
|
||||
|
||||
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", outputFile)));
|
||||
FXUtils.showFileInExplorer(outputFile);
|
||||
});
|
||||
return outputFile;
|
||||
}), Schedulers.io());
|
||||
}
|
||||
|
||||
private void onSponsor() {
|
||||
|
||||
@@ -22,8 +22,14 @@ import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import javafx.scene.control.TreeView;
|
||||
import javafx.scene.control.skin.TreeViewSkin;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import org.glavo.nbt.NBTElement;
|
||||
import org.glavo.nbt.tag.Tag;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
@@ -33,6 +39,7 @@ import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
@@ -56,8 +63,9 @@ public final class NBTEditorPage extends SpinnerPane implements DecoratorPage {
|
||||
|
||||
this.state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("nbt.title", file.toString())));
|
||||
this.file = file;
|
||||
this.type = NBTFileType.ofFile(file);
|
||||
|
||||
//noinspection DataFlowIssue
|
||||
this.type = NBTFileType.ofFile(file);
|
||||
if (type == null) {
|
||||
throw new IOException("Unknown type of file " + file);
|
||||
}
|
||||
@@ -85,14 +93,33 @@ public final class NBTEditorPage extends SpinnerPane implements DecoratorPage {
|
||||
|
||||
actions.getChildren().setAll(saveButton, cancelButton);
|
||||
|
||||
Task.supplyAsync(() -> type.readAsTree(file))
|
||||
Task.supplyAsync(() -> type.read(file))
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
setLoading(false);
|
||||
NBTTreeView view = new NBTTreeView(result);
|
||||
|
||||
NBTTreeItem root = new NBTTreeItem(result, FileUtils.getName(file));
|
||||
var view = new TreeView<>(root) {
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new TreeViewSkin<>(this) {
|
||||
{
|
||||
FXUtils.smoothScrolling(getVirtualFlow());
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
view.setCellFactory(ignored -> new NBTTreeCell());
|
||||
view.addEventHandler(TreeItem.<NBTElement>branchExpandedEvent(), event -> {
|
||||
TreeItem<NBTElement> item = event.getTreeItem();
|
||||
if (item.getValue() instanceof Tag && item.getChildren().size() == 1)
|
||||
item.getChildren().get(0).setExpanded(true);
|
||||
});
|
||||
root.setExpanded(true);
|
||||
|
||||
BorderPane.setMargin(view, new Insets(10));
|
||||
onEscPressed(view, cancelButton::fire);
|
||||
root.setCenter(view);
|
||||
this.root.setCenter(view);
|
||||
} else {
|
||||
LOG.warning("Fail to open nbt file", exception);
|
||||
Controllers.dialog(i18n("nbt.open.failed") + "\n\n" + StringUtils.getStackTrace(exception), null, MessageDialogPane.MessageType.WARNING, cancelButton::fire);
|
||||
|
||||
@@ -17,148 +17,34 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.nbt;
|
||||
|
||||
import com.github.steveice10.opennbt.NBTIO;
|
||||
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.IntTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.ListTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.Tag;
|
||||
import kala.compress.utils.BoundedInputStream;
|
||||
import org.glavo.nbt.NBTElement;
|
||||
import org.glavo.nbt.chunk.ChunkRegion;
|
||||
import org.glavo.nbt.io.NBTCodec;
|
||||
import org.glavo.nbt.tag.Tag;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.Inflater;
|
||||
import java.util.zip.InflaterInputStream;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
/// @author Glavo
|
||||
public enum NBTFileType {
|
||||
COMPRESSED("dat", "dat_old") {
|
||||
@Override
|
||||
public Tag read(Path file) throws IOException {
|
||||
try (BufferedInputStream fileInputStream = new BufferedInputStream(Files.newInputStream(file))) {
|
||||
fileInputStream.mark(3);
|
||||
byte[] header = new byte[3];
|
||||
if (fileInputStream.read(header) < 3) {
|
||||
throw new IOException("File is too small");
|
||||
}
|
||||
fileInputStream.reset();
|
||||
|
||||
InputStream input;
|
||||
if (Arrays.equals(header, new byte[]{0x1f, (byte) 0x8b, 0x08})) {
|
||||
input = new GZIPInputStream(fileInputStream);
|
||||
} else {
|
||||
input = fileInputStream;
|
||||
}
|
||||
|
||||
Tag tag = NBTIO.readTag(input);
|
||||
if (!(tag instanceof CompoundTag))
|
||||
throw new IOException("Unexpected tag: " + tag);
|
||||
return tag;
|
||||
}
|
||||
return NBTCodec.of().readTag(file);
|
||||
}
|
||||
},
|
||||
ANVIL("mca") {
|
||||
@Override
|
||||
public Tag read(Path file) throws IOException {
|
||||
return REGION.read(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NBTTreeView.Item readAsTree(Path file) throws IOException {
|
||||
return REGION.readAsTree(file);
|
||||
public NBTElement read(Path file) throws IOException {
|
||||
return NBTCodec.of().readRegion(file);
|
||||
}
|
||||
},
|
||||
REGION("mcr") {
|
||||
@Override
|
||||
public Tag read(Path file) throws IOException {
|
||||
try (RandomAccessFile r = new RandomAccessFile(file.toFile(), "r")) {
|
||||
ListTag tag = new ListTag(file.getFileName().toString(), CompoundTag.class);
|
||||
if (r.length() == 0) {
|
||||
return tag;
|
||||
}
|
||||
|
||||
byte[] header = new byte[4096];
|
||||
byte[] buffer = new byte[1 * 1024 * 1024]; // The maximum size of each chunk is 1MiB
|
||||
Inflater inflater = new Inflater();
|
||||
|
||||
r.readFully(header);
|
||||
for (int i = 0; i < 4096; i += 4) {
|
||||
int offset = ((header[i] & 0xff) << 16) + ((header[i + 1] & 0xff) << 8) + (header[i + 2] & 0xff);
|
||||
int length = header[i + 3] & 0xff;
|
||||
|
||||
if (offset == 0 || length == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
r.seek(offset * 4096L);
|
||||
r.readFully(buffer, 0, length * 4096);
|
||||
|
||||
int chunkLength = ((buffer[0] & 0xff) << 24) + ((buffer[1] & 0xff) << 16) + ((buffer[2] & 0xff) << 8) + (buffer[3] & 0xff);
|
||||
|
||||
InputStream input = new ByteArrayInputStream(buffer);
|
||||
input.skip(5);
|
||||
input = new BoundedInputStream(input, chunkLength - 1);
|
||||
|
||||
switch (buffer[4]) {
|
||||
case 0x01:
|
||||
// GZip
|
||||
input = new GZIPInputStream(input);
|
||||
break;
|
||||
case 0x02:
|
||||
// Zlib
|
||||
inflater.reset();
|
||||
input = new InflaterInputStream(input, inflater);
|
||||
break;
|
||||
case 0x03:
|
||||
// Uncompressed
|
||||
break;
|
||||
default:
|
||||
throw new IOException("Unsupported compression method: " + Integer.toHexString(buffer[4] & 0xff));
|
||||
}
|
||||
|
||||
try (InputStream in = input) {
|
||||
Tag chunk = NBTIO.readTag(in);
|
||||
if (!(chunk instanceof CompoundTag))
|
||||
throw new IOException("Unexpected tag: " + chunk);
|
||||
|
||||
tag.add(chunk);
|
||||
}
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public NBTTreeView.Item readAsTree(Path file) throws IOException {
|
||||
NBTTreeView.Item item = new NBTTreeView.Item(read(file));
|
||||
|
||||
for (Tag tag : ((ListTag) item.getValue())) {
|
||||
CompoundTag chunk = (CompoundTag) tag;
|
||||
|
||||
NBTTreeView.Item tree = NBTTreeView.buildTree(chunk);
|
||||
|
||||
Tag xPos = chunk.get("xPos");
|
||||
Tag zPos = chunk.get("zPos");
|
||||
|
||||
if (xPos instanceof IntTag && zPos instanceof IntTag) {
|
||||
tree.setText(String.format("Chunk: %d %d", xPos.getValue(), zPos.getValue()));
|
||||
} else {
|
||||
tree.setText("Chunk: Unknown");
|
||||
}
|
||||
|
||||
item.getChildren().add(tree);
|
||||
}
|
||||
|
||||
return item;
|
||||
public ChunkRegion read(Path file) throws IOException {
|
||||
return NBTCodec.of().readRegion(file);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -168,7 +54,7 @@ public enum NBTFileType {
|
||||
return NBTFileType.ofFile(file) != null;
|
||||
}
|
||||
|
||||
public static NBTFileType ofFile(Path file) {
|
||||
public static @Nullable NBTFileType ofFile(Path file) {
|
||||
String ext = FileUtils.getExtension(file);
|
||||
for (NBTFileType type : types) {
|
||||
for (String extension : type.extensions) {
|
||||
@@ -186,11 +72,6 @@ public enum NBTFileType {
|
||||
this.extensions = extensions;
|
||||
}
|
||||
|
||||
public abstract Tag read(Path file) throws IOException;
|
||||
public abstract NBTElement read(Path file) throws IOException;
|
||||
|
||||
public NBTTreeView.Item readAsTree(Path file) throws IOException {
|
||||
NBTTreeView.Item root = NBTTreeView.buildTree(read(file));
|
||||
root.setName(file.getFileName().toString());
|
||||
return root;
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* 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.nbt;
|
||||
|
||||
import com.github.steveice10.opennbt.tag.builtin.Tag;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public enum NBTTagType {
|
||||
BYTE, SHORT, INT, LONG, FLOAT, DOUBLE,
|
||||
BYTE_ARRAY, INT_ARRAY, LONG_ARRAY,
|
||||
STRING,
|
||||
LIST, COMPOUND;
|
||||
|
||||
private static final Map<String, NBTTagType> lookupTable = new HashMap<>();
|
||||
|
||||
static {
|
||||
for (NBTTagType type : values()) {
|
||||
lookupTable.put(type.getTagClassName(), type);
|
||||
}
|
||||
}
|
||||
|
||||
public static NBTTagType typeOf(Tag tag) {
|
||||
NBTTagType type = lookupTable.get(tag.getClass().getSimpleName());
|
||||
if (type == null) {
|
||||
throw new IllegalArgumentException("Unknown tag: " + type);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
private final String iconUrl;
|
||||
private final String tagClassName;
|
||||
|
||||
NBTTagType() {
|
||||
String tagName;
|
||||
String className;
|
||||
|
||||
int idx = name().indexOf('_');
|
||||
if (idx < 0) {
|
||||
tagName = name().charAt(0) + name().substring(1).toLowerCase(Locale.ROOT);
|
||||
className = tagName + "Tag";
|
||||
} else {
|
||||
tagName = name().charAt(0) + name().substring(1, idx + 1).toLowerCase(Locale.ROOT)
|
||||
+ name().charAt(idx + 1) + name().substring(idx + 2).toLowerCase(Locale.ROOT);
|
||||
className = tagName.substring(0, idx) + tagName.substring(idx + 1) + "Tag";
|
||||
}
|
||||
|
||||
this.iconUrl = "/assets/img/nbt/TAG_" + tagName + ".png";
|
||||
this.tagClassName = className;
|
||||
}
|
||||
|
||||
public String getIconUrl() {
|
||||
return iconUrl;
|
||||
}
|
||||
|
||||
public String getTagClassName() {
|
||||
return tagClassName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.nbt;
|
||||
|
||||
import javafx.scene.control.TreeCell;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import org.glavo.nbt.NBTElement;
|
||||
import org.glavo.nbt.chunk.Chunk;
|
||||
import org.glavo.nbt.chunk.ChunkRegion;
|
||||
import org.glavo.nbt.tag.*;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class NBTTreeCell extends TreeCell<@Nullable NBTElement> {
|
||||
|
||||
private static @Nullable Image getIcon(NBTElement element) {
|
||||
if (element instanceof Tag tag) {
|
||||
TagType<?> type = tag.getType();
|
||||
if (type == TagType.BYTE) {
|
||||
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Byte.png");
|
||||
} else if (type == TagType.SHORT) {
|
||||
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Short.png");
|
||||
} else if (type == TagType.INT) {
|
||||
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Int.png");
|
||||
} else if (type == TagType.LONG) {
|
||||
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Long.png");
|
||||
} else if (type == TagType.FLOAT) {
|
||||
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Float.png");
|
||||
} else if (type == TagType.DOUBLE) {
|
||||
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Double.png");
|
||||
} else if (type == TagType.STRING) {
|
||||
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_String.png");
|
||||
} else if (type == TagType.BYTE_ARRAY) {
|
||||
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Byte_Array.png");
|
||||
} else if (type == TagType.INT_ARRAY) {
|
||||
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Int_Array.png");
|
||||
} else if (type == TagType.LONG_ARRAY) {
|
||||
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Long_Array.png");
|
||||
} else if (type == TagType.LIST) {
|
||||
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_List.png");
|
||||
} else if (type == TagType.COMPOUND) {
|
||||
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Compound.png");
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else if (element instanceof ChunkRegion)
|
||||
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_List.png");
|
||||
else if (element instanceof Chunk)
|
||||
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Compound.png");
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
private final ImageView imageView;
|
||||
|
||||
public NBTTreeCell() {
|
||||
imageView = new ImageView();
|
||||
imageView.setFitHeight(16);
|
||||
imageView.setFitWidth(16);
|
||||
setGraphic(imageView);
|
||||
}
|
||||
|
||||
private NBTTreeItem getNBTTreeItem() {
|
||||
return (NBTTreeItem) getTreeItem();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateItem(@Nullable NBTElement item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (empty || item == null) {
|
||||
imageView.setImage(null);
|
||||
setText(null);
|
||||
return;
|
||||
}
|
||||
|
||||
imageView.setImage(getIcon(item));
|
||||
|
||||
String name = getNBTTreeItem().getOverrideName();
|
||||
if (name == null) {
|
||||
NBTElement value = getNBTTreeItem().getValue();
|
||||
|
||||
if (value instanceof Tag tag) {
|
||||
if (tag.getParent() instanceof ListTag<?>) {
|
||||
name = Integer.toString(tag.getIndex());
|
||||
} else {
|
||||
name = tag.getName();
|
||||
}
|
||||
} else if (value instanceof Chunk chunk) {
|
||||
name = "Chunk (" + chunk.getLocalX() + ", " + chunk.getLocalZ() + ")";
|
||||
} else {
|
||||
name = "";
|
||||
}
|
||||
}
|
||||
|
||||
String text;
|
||||
if (item instanceof ParentTag<?> parentTag) {
|
||||
text = i18n("nbt.entries", parentTag.size());
|
||||
} else if (item instanceof ValueTag<?> valueTag) {
|
||||
text = valueTag.getAsString();
|
||||
} else {
|
||||
text = null;
|
||||
}
|
||||
|
||||
if (text == null) {
|
||||
setText(name);
|
||||
} else {
|
||||
setText(name + ": " + text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.nbt;
|
||||
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import org.glavo.nbt.NBTElement;
|
||||
import org.glavo.nbt.NBTParent;
|
||||
import org.glavo.nbt.chunk.Chunk;
|
||||
import org.glavo.nbt.tag.*;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/// @author Glavo
|
||||
public final class NBTTreeItem extends TreeItem<NBTElement> {
|
||||
private final @Nullable String overrideName;
|
||||
|
||||
public NBTTreeItem(NBTElement value) {
|
||||
this(value, null);
|
||||
}
|
||||
|
||||
public NBTTreeItem(NBTElement value, @Nullable String overrideName) {
|
||||
super(value);
|
||||
this.overrideName = overrideName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLeaf() {
|
||||
return !(getValue() instanceof NBTParent<?> parent) || parent.isEmpty();
|
||||
}
|
||||
|
||||
private boolean needInitChildren = true;
|
||||
|
||||
@Override
|
||||
public ObservableList<TreeItem<NBTElement>> getChildren() {
|
||||
ObservableList<TreeItem<NBTElement>> children = super.getChildren();
|
||||
if (needInitChildren) {
|
||||
needInitChildren = false;
|
||||
|
||||
if (getValue() instanceof Chunk chunk) {
|
||||
if (chunk.getRootTag() != null) {
|
||||
children.setAll(chunk.getRootTag().stream().map(NBTTreeItem::new).toList());
|
||||
}
|
||||
} else if (getValue() instanceof NBTParent<?> parent) {
|
||||
children.setAll(parent.stream().map(NBTTreeItem::new).toList());
|
||||
}
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
public @Nullable String getOverrideName() {
|
||||
return overrideName;
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
/*
|
||||
* 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.nbt;
|
||||
|
||||
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.ListTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.Tag;
|
||||
import javafx.scene.control.TreeCell;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import javafx.scene.control.TreeView;
|
||||
import javafx.scene.control.skin.TreeViewSkin;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.util.Callback;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.EnumMap;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class NBTTreeView extends TreeView<Tag> {
|
||||
|
||||
public NBTTreeView(NBTTreeView.Item tree) {
|
||||
this.setRoot(tree);
|
||||
if (tree != null) tree.setExpanded(true);
|
||||
this.setCellFactory(cellFactory());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected javafx.scene.control.Skin<?> createDefaultSkin() {
|
||||
return new TreeViewSkin<Tag>(this) {
|
||||
{
|
||||
FXUtils.smoothScrolling(getVirtualFlow());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static Callback<TreeView<Tag>, TreeCell<Tag>> cellFactory() {
|
||||
EnumMap<NBTTagType, Image> icons = new EnumMap<>(NBTTagType.class);
|
||||
|
||||
return view -> new TreeCell<>() {
|
||||
private void setTagText(String text) {
|
||||
String name = ((Item) getTreeItem()).getName();
|
||||
|
||||
if (name == null) {
|
||||
setText(text);
|
||||
} else if (text == null) {
|
||||
setText(name);
|
||||
} else {
|
||||
setText(name + ": " + text);
|
||||
}
|
||||
}
|
||||
|
||||
private void setTagText(int nEntries) {
|
||||
setTagText(i18n("nbt.entries", nEntries));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateItem(Tag item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
ImageView imageView = (ImageView) this.getGraphic();
|
||||
if (imageView == null) {
|
||||
imageView = new ImageView();
|
||||
this.setGraphic(imageView);
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
imageView.setImage(null);
|
||||
setText(null);
|
||||
return;
|
||||
}
|
||||
|
||||
NBTTagType tagType = NBTTagType.typeOf(item);
|
||||
imageView.setImage(icons.computeIfAbsent(tagType, type -> new Image(type.getIconUrl())));
|
||||
imageView.setFitHeight(16);
|
||||
imageView.setFitWidth(16);
|
||||
|
||||
if (((Item) getTreeItem()).getText() != null) {
|
||||
setText(((Item) getTreeItem()).getText());
|
||||
} else {
|
||||
switch (tagType) {
|
||||
case BYTE:
|
||||
case SHORT:
|
||||
case INT:
|
||||
case LONG:
|
||||
case FLOAT:
|
||||
case DOUBLE:
|
||||
case STRING:
|
||||
setTagText(item.getValue().toString());
|
||||
break;
|
||||
case BYTE_ARRAY:
|
||||
case INT_ARRAY:
|
||||
case LONG_ARRAY:
|
||||
setTagText(Array.getLength(item.getValue()));
|
||||
break;
|
||||
case LIST:
|
||||
setTagText(((ListTag) item).size());
|
||||
break;
|
||||
case COMPOUND:
|
||||
setTagText(((CompoundTag) item).size());
|
||||
break;
|
||||
default:
|
||||
setTagText(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static Item buildTree(Tag tag) {
|
||||
Item item = new Item(tag);
|
||||
|
||||
if (tag instanceof CompoundTag) {
|
||||
for (Tag subTag : ((CompoundTag) tag)) {
|
||||
item.getChildren().add(buildTree(subTag));
|
||||
}
|
||||
} else if (tag instanceof ListTag) {
|
||||
int idx = 0;
|
||||
for (Tag subTag : ((ListTag) tag)) {
|
||||
Item subTree = buildTree(subTag);
|
||||
subTree.setName(String.valueOf(idx++));
|
||||
item.getChildren().add(subTree);
|
||||
}
|
||||
}
|
||||
FXUtils.onChangeAndOperate(item.expandedProperty(), expanded -> {
|
||||
if (expanded && item.getChildren().size() == 1) item.getChildren().get(0).setExpanded(true);
|
||||
});
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
public CompoundTag getRootTag() {
|
||||
return ((CompoundTag) getRoot().getValue());
|
||||
}
|
||||
|
||||
public static class Item extends TreeItem<Tag> {
|
||||
|
||||
private String text;
|
||||
private String name;
|
||||
|
||||
public Item() {
|
||||
}
|
||||
|
||||
public Item(Tag value) {
|
||||
super(value);
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name == null ? getValue().getName() : name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@NotNullByDefault
|
||||
package org.jackhuang.hmcl.ui.nbt;
|
||||
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
@@ -195,9 +212,11 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
|
||||
useNativeGLFWPane = new LineToggleButton();
|
||||
useNativeGLFWPane.setTitle(i18n("settings.advanced.use_native_glfw"));
|
||||
useNativeGLFWPane.setSubtitle(i18n("settings.advanced.linux_freebsd_only"));
|
||||
|
||||
useNativeOpenALPane = new LineToggleButton();
|
||||
useNativeOpenALPane.setTitle(i18n("settings.advanced.use_native_openal"));
|
||||
useNativeOpenALPane.setSubtitle(i18n("settings.advanced.linux_freebsd_only"));
|
||||
|
||||
workaroundPane.getContent().setAll(
|
||||
nativesDirSublist, rendererPane, noJVMArgsPane, noOptimizingJVMArgsPane, noGameCheckPane,
|
||||
|
||||
@@ -101,6 +101,10 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
this.downloadProvider = DownloadProviders.getDownloadProvider();
|
||||
}
|
||||
|
||||
public DownloadProvider getDownloadProvider() {
|
||||
return downloadProvider;
|
||||
}
|
||||
|
||||
public ObservableList<Node> getActions() {
|
||||
return actions;
|
||||
}
|
||||
@@ -233,18 +237,20 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
|
||||
private static class ModDownloadListPageSkin extends SkinBase<DownloadListPage> {
|
||||
private final JFXListView<RemoteMod> listView = new JFXListView<>();
|
||||
private final RemoteImageLoader iconLoader = new RemoteImageLoader() {
|
||||
@Override
|
||||
protected @NotNull Task<Image> createLoadTask(@NotNull URI uri) {
|
||||
return FXUtils.getRemoteImageTask(uri, 80, 80, true, true);
|
||||
}
|
||||
};
|
||||
private final RemoteImageLoader iconLoader;
|
||||
|
||||
protected ModDownloadListPageSkin(DownloadListPage control) {
|
||||
super(control);
|
||||
|
||||
listView.getStyleClass().add("no-horizontal-scrollbar");
|
||||
|
||||
iconLoader = new RemoteImageLoader(control.downloadProvider) {
|
||||
@Override
|
||||
protected @NotNull Task<Image> createLoadTask(@NotNull List<URI> uris) {
|
||||
return FXUtils.getRemoteImageTask(uris, 80, 80, true, true);
|
||||
}
|
||||
};
|
||||
|
||||
BorderPane pane = new BorderPane();
|
||||
|
||||
GridPane searchPane = new GridPane();
|
||||
|
||||
@@ -38,6 +38,7 @@ import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
@@ -112,7 +113,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
setFailed(false);
|
||||
|
||||
Task.supplyAsync(() -> {
|
||||
Stream<RemoteMod.Version> versions = addon.getData().loadVersions(repository);
|
||||
Stream<RemoteMod.Version> versions = addon.getData().loadVersions(repository, page.getDownloadProvider());
|
||||
return sortVersions(versions);
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception == null) {
|
||||
@@ -179,7 +180,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
if (this.callback == null) {
|
||||
saveAs(mod, file);
|
||||
} else {
|
||||
this.callback.download(version.getProfile(), version.getVersion(), mod, file);
|
||||
this.callback.download(page.getDownloadProvider(), version.getProfile(), version.getVersion(), mod, file);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,7 +549,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
dependencies.put(dependency.getType(), list);
|
||||
}
|
||||
|
||||
queue.add(Task.supplyAsync(Schedulers.io(), dependency::load)
|
||||
queue.add(Task.supplyAsync(Schedulers.io(), () -> dependency.load(selfPage.page.getDownloadProvider()))
|
||||
.setSignificance(Task.TaskSignificance.MINOR)
|
||||
.thenAcceptAsync(Schedulers.javafx(), dep -> {
|
||||
if (dep == RemoteMod.BROKEN) {
|
||||
@@ -576,7 +577,8 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface DownloadCallback {
|
||||
void download(Profile profile, @Nullable String version, RemoteMod mod, RemoteMod.Version file);
|
||||
void download(DownloadProvider downloadProvider, Profile profile, @Nullable String version, RemoteMod mod, RemoteMod.Version file);
|
||||
}
|
||||
}
|
||||
+7
-2
@@ -50,7 +50,7 @@ public final class HMCLLocalizedDownloadListPage extends DownloadListPage {
|
||||
}
|
||||
|
||||
public static DownloadListPage ofShaderPack(DownloadPage.DownloadCallback callback, boolean versionSelection) {
|
||||
var page = new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteModRepository.Type.SHADER_PACK, null, ModrinthRemoteModRepository.SHADER_PACKS);
|
||||
var page = new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteModRepository.Type.SHADER_PACK, CurseForgeRemoteModRepository.SHADERS, ModrinthRemoteModRepository.SHADER_PACKS);
|
||||
page.supportChinese.set(false);
|
||||
return page;
|
||||
}
|
||||
@@ -61,7 +61,12 @@ public final class HMCLLocalizedDownloadListPage extends DownloadListPage {
|
||||
repository = new Repository(type, curseForge, modrinth);
|
||||
|
||||
supportChinese.set(true);
|
||||
downloadSources.get().setAll("mods.modrinth", "mods.curseforge");
|
||||
|
||||
downloadSources.setAll("mods.modrinth");
|
||||
if (CurseForgeRemoteModRepository.isAvailable()) {
|
||||
downloadSources.add("mods.curseforge");
|
||||
}
|
||||
|
||||
if (modrinth != null) {
|
||||
downloadSource.set("mods.modrinth");
|
||||
} else if (curseForge != null) {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -30,16 +31,18 @@ import java.util.Objects;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class ModCheckUpdatesTask extends Task<List<LocalModFile.ModUpdate>> {
|
||||
private final DownloadProvider downloadProvider;
|
||||
private final List<Task<LocalModFile.ModUpdate>> dependents;
|
||||
|
||||
public ModCheckUpdatesTask(String gameVersion, Collection<LocalModFile> mods) {
|
||||
public ModCheckUpdatesTask(DownloadProvider downloadProvider, String gameVersion, Collection<LocalModFile> mods) {
|
||||
this.downloadProvider = downloadProvider;
|
||||
dependents = mods.stream().map(mod ->
|
||||
Task.supplyAsync(Schedulers.io(), () -> {
|
||||
LocalModFile.ModUpdate candidate = null;
|
||||
for (RemoteMod.Type type : RemoteMod.Type.values()) {
|
||||
LocalModFile.ModUpdate update = null;
|
||||
try {
|
||||
update = mod.checkUpdates(gameVersion, type.getRemoteModRepository());
|
||||
update = mod.checkUpdates(downloadProvider, gameVersion, type.getRemoteModRepository());
|
||||
} catch (IOException e) {
|
||||
LOG.warning(String.format("Cannot check update for mod %s.", mod.getFileName()), e);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -240,7 +241,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
|
||||
.composeAsync(() -> {
|
||||
Optional<String> gameVersion = profile.getRepository().getGameVersion(instanceId);
|
||||
if (gameVersion.isPresent()) {
|
||||
return new ModCheckUpdatesTask(gameVersion.get(), mods);
|
||||
return new ModCheckUpdatesTask(DownloadProviders.getDownloadProvider(), gameVersion.get(), mods);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
|
||||
@@ -43,6 +43,7 @@ import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.VersionIconType;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -465,7 +466,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
Task.runAsync(() -> {
|
||||
Optional<RemoteMod.Version> versionOptional = repository.getRemoteVersionByLocalFile(modInfo.getModInfo(), modInfo.getModInfo().getFile());
|
||||
if (versionOptional.isPresent()) {
|
||||
RemoteMod remoteMod = repository.getModById(versionOptional.get().getModid());
|
||||
RemoteMod remoteMod = repository.getModById(DownloadProviders.getDownloadProvider(), versionOptional.get().getModid());
|
||||
FXUtils.runInFX(() -> {
|
||||
for (ModLoaderType modLoaderType : versionOptional.get().getLoaders()) {
|
||||
String loaderName = switch (modLoaderType) {
|
||||
@@ -493,7 +494,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
repository instanceof CurseForgeRemoteModRepository ? HMCLLocalizedDownloadListPage.ofCurseForgeMod(null, false) : HMCLLocalizedDownloadListPage.ofModrinthMod(null, false),
|
||||
remoteMod,
|
||||
new Profile.ProfileVersion(ModListPageSkin.this.getSkinnable().getProfile(), ModListPageSkin.this.getSkinnable().getInstanceId()),
|
||||
(profile, version, mod, file) -> org.jackhuang.hmcl.ui.download.DownloadPage.download(profile, version, file, "mods")
|
||||
(downloadProvider, profile, version, mod, file) -> org.jackhuang.hmcl.ui.download.DownloadPage.download(downloadProvider, profile, version, file, "mods")
|
||||
));
|
||||
});
|
||||
button.setDisable(false);
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
@@ -43,6 +42,7 @@ import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.MemoryStatusBar;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
@@ -57,7 +57,6 @@ import org.jackhuang.hmcl.util.platform.SystemInfo;
|
||||
import org.jackhuang.hmcl.util.platform.hardware.PhysicalMemoryStatus;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@@ -115,8 +114,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
private final IntegerProperty maxMemory = new SimpleIntegerProperty();
|
||||
private final BooleanProperty modpack = new SimpleBooleanProperty();
|
||||
|
||||
private final ReadOnlyObjectProperty<PhysicalMemoryStatus> memoryStatus = UpdateMemoryStatus.memoryStatusProperty();
|
||||
|
||||
public VersionSettingsPage(boolean globalSetting) {
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
scrollPane.setFitToHeight(true);
|
||||
@@ -296,30 +293,14 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
lowerBoundPane.getChildren().setAll(label, slider, txtMaxMemory, new Label(i18n("settings.memory.unit.mib")));
|
||||
}
|
||||
|
||||
StackPane progressBarPane = new StackPane();
|
||||
progressBarPane.setAlignment(Pos.CENTER_LEFT);
|
||||
VBox.setMargin(progressBarPane, new Insets(8, 0, 0, 16));
|
||||
{
|
||||
progressBarPane.setMinHeight(4);
|
||||
progressBarPane.getStyleClass().add("memory-total");
|
||||
|
||||
StackPane usedMemory = new StackPane();
|
||||
usedMemory.getStyleClass().add("memory-used");
|
||||
usedMemory.maxWidthProperty().bind(Bindings.createDoubleBinding(() ->
|
||||
progressBarPane.getWidth() *
|
||||
(memoryStatus.get().getUsed() * 1.0 / memoryStatus.get().getTotal()), progressBarPane.widthProperty(),
|
||||
memoryStatus));
|
||||
StackPane allocateMemory = new StackPane();
|
||||
allocateMemory.getStyleClass().add("memory-allocate");
|
||||
allocateMemory.maxWidthProperty().bind(Bindings.createDoubleBinding(() ->
|
||||
progressBarPane.getWidth() *
|
||||
Math.min(1.0,
|
||||
(double) (HMCLGameRepository.getAllocatedMemory(maxMemory.get() * 1024L * 1024L, memoryStatus.get().getAvailable(), chkAutoAllocate.isSelected())
|
||||
+ memoryStatus.get().getUsed()) / memoryStatus.get().getTotal()), progressBarPane.widthProperty(),
|
||||
maxMemory, memoryStatus, chkAutoAllocate.selectedProperty()));
|
||||
|
||||
progressBarPane.getChildren().setAll(allocateMemory, usedMemory);
|
||||
}
|
||||
MemoryStatusBar memoryStatusBar = new MemoryStatusBar();
|
||||
VBox.setMargin(memoryStatusBar, new Insets(8, 0, 0, 16));
|
||||
memoryStatusBar.memoryAllocatedProperty().bind(Bindings.createDoubleBinding(() ->
|
||||
(double) HMCLGameRepository.getAllocatedMemory(
|
||||
maxMemory.get() * 1024L * 1024L,
|
||||
memoryStatusBar.getMemoryStatus().getAvailable(),
|
||||
chkAutoAllocate.isSelected()),
|
||||
memoryStatusBar.memoryStatusProperty(), maxMemory, chkAutoAllocate.selectedProperty()));
|
||||
|
||||
BorderPane digitalPane = new BorderPane();
|
||||
VBox.setMargin(digitalPane, new Insets(0, 0, 0, 16));
|
||||
@@ -327,26 +308,29 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
Label lblPhysicalMemory = new Label();
|
||||
lblPhysicalMemory.getStyleClass().add("memory-label");
|
||||
digitalPane.setLeft(lblPhysicalMemory);
|
||||
lblPhysicalMemory.textProperty().bind(Bindings.createStringBinding(() ->
|
||||
i18n("settings.memory.used_per_total",
|
||||
GIGABYTES.convertFromBytes(memoryStatus.get().getUsed()),
|
||||
GIGABYTES.convertFromBytes(memoryStatus.get().getTotal())), memoryStatus));
|
||||
lblPhysicalMemory.textProperty().bind(Bindings.createStringBinding(() -> {
|
||||
PhysicalMemoryStatus memoryStatus = memoryStatusBar.getMemoryStatus();
|
||||
return i18n("settings.memory.used_per_total",
|
||||
GIGABYTES.convertFromBytes(memoryStatus.getUsed()),
|
||||
GIGABYTES.convertFromBytes(memoryStatus.getTotal()));
|
||||
}, memoryStatusBar.memoryStatusProperty()));
|
||||
|
||||
Label lblAllocateMemory = new Label();
|
||||
lblAllocateMemory.textProperty().bind(Bindings.createStringBinding(() -> {
|
||||
PhysicalMemoryStatus memoryStatus = memoryStatusBar.getMemoryStatus();
|
||||
long maxMemory = Lang.parseInt(this.maxMemory.get(), 0) * 1024L * 1024L;
|
||||
return i18n(memoryStatus.get().hasAvailable() && maxMemory > memoryStatus.get().getAvailable()
|
||||
return i18n(memoryStatus.hasAvailable() && maxMemory > memoryStatus.getAvailable()
|
||||
? (chkAutoAllocate.isSelected() ? "settings.memory.allocate.auto.exceeded" : "settings.memory.allocate.manual.exceeded")
|
||||
: (chkAutoAllocate.isSelected() ? "settings.memory.allocate.auto" : "settings.memory.allocate.manual"),
|
||||
GIGABYTES.convertFromBytes(maxMemory),
|
||||
GIGABYTES.convertFromBytes(HMCLGameRepository.getAllocatedMemory(maxMemory, memoryStatus.get().getAvailable(), chkAutoAllocate.isSelected())),
|
||||
GIGABYTES.convertFromBytes(memoryStatus.get().getAvailable()));
|
||||
}, memoryStatus, maxMemory, chkAutoAllocate.selectedProperty()));
|
||||
GIGABYTES.convertFromBytes(HMCLGameRepository.getAllocatedMemory(maxMemory, memoryStatus.getAvailable(), chkAutoAllocate.isSelected())),
|
||||
GIGABYTES.convertFromBytes(memoryStatus.getAvailable()));
|
||||
}, memoryStatusBar.memoryStatusProperty(), maxMemory, chkAutoAllocate.selectedProperty()));
|
||||
lblAllocateMemory.getStyleClass().add("memory-label");
|
||||
digitalPane.setRight(lblAllocateMemory);
|
||||
}
|
||||
|
||||
maxMemoryPane.getChildren().setAll(title, chkAutoAllocate, lowerBoundPane, progressBarPane, digitalPane);
|
||||
maxMemoryPane.getChildren().setAll(title, chkAutoAllocate, lowerBoundPane, memoryStatusBar, digitalPane);
|
||||
}
|
||||
|
||||
launcherVisibilityPane = new LineSelectButton<>();
|
||||
@@ -472,7 +456,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
|
||||
componentList.disableProperty().bind(enableSpecificSettings.not());
|
||||
}
|
||||
|
||||
|
||||
private void fireWorkingDirChanged() {
|
||||
FXUtils.runInFX(() -> fireEvent(new VersionPage.WorkingDirChangedEvent()));
|
||||
}
|
||||
@@ -776,57 +760,4 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
return state.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
private static final class UpdateMemoryStatus extends Thread {
|
||||
|
||||
@FXThread
|
||||
private static WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyCache;
|
||||
|
||||
@FXThread
|
||||
static ReadOnlyObjectProperty<PhysicalMemoryStatus> memoryStatusProperty() {
|
||||
if (memoryStatusPropertyCache != null) {
|
||||
var property = memoryStatusPropertyCache.get();
|
||||
if (property != null) {
|
||||
return property;
|
||||
}
|
||||
}
|
||||
|
||||
ObjectProperty<PhysicalMemoryStatus> property = new SimpleObjectProperty<>(PhysicalMemoryStatus.INVALID);
|
||||
memoryStatusPropertyCache = new WeakReference<>(property);
|
||||
new UpdateMemoryStatus(memoryStatusPropertyCache).start();
|
||||
return property;
|
||||
}
|
||||
|
||||
private final WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyRef;
|
||||
|
||||
UpdateMemoryStatus(WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyRef) {
|
||||
this.memoryStatusPropertyRef = memoryStatusPropertyRef;
|
||||
|
||||
setName("UpdateMemoryStatus");
|
||||
setDaemon(true);
|
||||
setPriority(Thread.MIN_PRIORITY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while (true) {
|
||||
PhysicalMemoryStatus status = SystemInfo.getPhysicalMemoryStatus();
|
||||
|
||||
var memoryStatusProperty = memoryStatusPropertyRef.get();
|
||||
if (memoryStatusProperty == null)
|
||||
return;
|
||||
|
||||
if (Controllers.isStopped())
|
||||
return;
|
||||
|
||||
Platform.runLater(() -> memoryStatusProperty.set(status));
|
||||
|
||||
try {
|
||||
//noinspection BusyWait
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import javafx.application.Platform;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
@@ -42,13 +43,13 @@ import org.jackhuang.hmcl.ui.export.ExportWizardProvider;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
@@ -72,11 +73,11 @@ public final class Versions {
|
||||
}
|
||||
}
|
||||
|
||||
public static void downloadModpackImpl(Profile profile, String version, RemoteMod mod, RemoteMod.Version file) {
|
||||
public static void downloadModpackImpl(DownloadProvider downloadProvider, Profile profile, String version, RemoteMod mod, RemoteMod.Version file) {
|
||||
Path modpack;
|
||||
URI downloadURL;
|
||||
List<URI> downloadURLs;
|
||||
try {
|
||||
downloadURL = NetworkUtils.toURI(file.getFile().getUrl());
|
||||
downloadURLs = downloadProvider.injectURLWithCandidates(file.getFile().getUrl());
|
||||
modpack = Files.createTempFile("modpack", ".zip");
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
Controllers.dialog(
|
||||
@@ -85,7 +86,7 @@ public final class Versions {
|
||||
return;
|
||||
}
|
||||
Controllers.taskDialog(
|
||||
new FileDownloadTask(downloadURL, modpack)
|
||||
new FileDownloadTask(downloadURLs, modpack)
|
||||
.whenComplete(Schedulers.javafx(), e -> {
|
||||
if (e == null) {
|
||||
ModpackInstallWizardProvider installWizardProvider;
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.github.steveice10.opennbt.tag.builtin.*;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
@@ -34,6 +33,7 @@ import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.glavo.nbt.tag.*;
|
||||
import org.jackhuang.hmcl.game.World;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -76,7 +76,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
}
|
||||
|
||||
private void updateControls() {
|
||||
CompoundTag dataTag = levelData.get("Data");
|
||||
CompoundTag dataTag = (CompoundTag) levelData.get("Data");
|
||||
CompoundTag playerTag = playerData;
|
||||
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
@@ -100,7 +100,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
setRightTextField(worldNamePane, worldNameField, 200);
|
||||
|
||||
if (dataTag.get("LevelName") instanceof StringTag worldNameTag) {
|
||||
var worldName = new SimpleStringProperty(worldNameTag.getValue());
|
||||
var worldName = new SimpleStringProperty(worldNameTag.get());
|
||||
FXUtils.bindString(worldNameField, worldName);
|
||||
worldNameField.getProperties().put(WorldInfoPage.class.getName() + ".worldNameProperty", worldName);
|
||||
worldName.addListener((observable, oldValue, newValue) -> {
|
||||
@@ -324,7 +324,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
{
|
||||
locationPane.setTitle(i18n("world.info.player.location"));
|
||||
Dimension dimension = Dimension.of(playerTag.get("Dimension"));
|
||||
if (dimension != null && playerTag.get("Pos") instanceof ListTag posTag) {
|
||||
if (dimension != null && playerTag.get("Pos") instanceof ListTag<?> posTag) {
|
||||
locationPane.setText(dimension.formatPosition(posTag));
|
||||
}
|
||||
}
|
||||
@@ -347,7 +347,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
if (playerTag.get("respawn") instanceof CompoundTag respawnTag
|
||||
&& respawnTag.get("dimension") instanceof StringTag dimensionTag
|
||||
&& respawnTag.get("pos") instanceof IntArrayTag intArrayTag
|
||||
&& intArrayTag.length() >= 3) { // Valid after 25w07a
|
||||
&& intArrayTag.size() >= 3) { // Valid after 25w07a
|
||||
spawnPane.setText(Dimension.of(dimensionTag).formatPosition(intArrayTag));
|
||||
} else if (playerTag.get("SpawnX") instanceof IntTag intX
|
||||
&& playerTag.get("SpawnY") instanceof IntTag intY
|
||||
@@ -541,7 +541,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
|
||||
static Dimension of(Tag tag) {
|
||||
if (tag instanceof IntTag intTag) {
|
||||
return switch (intTag.getValue()) {
|
||||
return switch (intTag.get()) {
|
||||
case 0 -> OVERWORLD;
|
||||
case -1 -> THE_NETHER;
|
||||
case 1 -> THE_END;
|
||||
@@ -561,30 +561,23 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
}
|
||||
|
||||
String formatPosition(Tag tag) {
|
||||
if (tag instanceof ListTag listTag && listTag.size() == 3) {
|
||||
if (tag instanceof ListTag<?> listTag && listTag.size() == 3) {
|
||||
|
||||
Tag x = listTag.get(0);
|
||||
Tag y = listTag.get(1);
|
||||
Tag z = listTag.get(2);
|
||||
|
||||
if (x instanceof DoubleTag && y instanceof DoubleTag && z instanceof DoubleTag) {
|
||||
return this == OVERWORLD
|
||||
? String.format("(%.2f, %.2f, %.2f)", x.getValue(), y.getValue(), z.getValue())
|
||||
: String.format("%s (%.2f, %.2f, %.2f)", name, x.getValue(), y.getValue(), z.getValue());
|
||||
if (listTag.getTag(0) instanceof DoubleTag x
|
||||
&& listTag.getTag(1) instanceof DoubleTag y
|
||||
&& listTag.getTag(2) instanceof DoubleTag z) {
|
||||
return formatPosition(x.get(), y.get(), z.get());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (tag instanceof IntArrayTag intArrayTag) {
|
||||
int x = intArrayTag.get(0);
|
||||
int y = intArrayTag.get(1);
|
||||
int z = intArrayTag.get(2);
|
||||
|
||||
int x = intArrayTag.getValue(0);
|
||||
int y = intArrayTag.getValue(1);
|
||||
int z = intArrayTag.getValue(2);
|
||||
|
||||
return this == OVERWORLD
|
||||
? String.format("(%d, %d, %d)", x, y, z)
|
||||
: String.format("%s (%d, %d, %d)", name, x, y, z);
|
||||
return formatPosition(x, y, z);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -592,14 +585,14 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
|
||||
|
||||
String formatPosition(int x, int y, int z) {
|
||||
return this == OVERWORLD
|
||||
? String.format("(%d, %d, %d)", x, y, z)
|
||||
: String.format("%s (%d, %d, %d)", name, x, y, z);
|
||||
? "(%d, %d, %d)".formatted(x, y, z)
|
||||
: "%s (%d, %d, %d)".formatted(name, x, y, z);
|
||||
}
|
||||
|
||||
String formatPosition(double x, double y, double z) {
|
||||
return this == OVERWORLD
|
||||
? String.format("(%.2f, %.2f, %.2f)", x, y, z)
|
||||
: String.format("%s (%.2f, %.2f, %.2f)", name, x, y, z);
|
||||
? "(%.2f, %.2f, %.2f)".formatted(x, y, z)
|
||||
: "%s (%.2f, %.2f, %.2f)".formatted(name, x, y, z);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
@@ -174,15 +175,25 @@ public final class UpdateHandler {
|
||||
startJava(updateTo, "--apply-to", self.toString());
|
||||
}
|
||||
|
||||
private static void startJava(Path jar, String... appArgs) throws IOException {
|
||||
public static void startJava(Path jar, String... appArgs) throws IOException {
|
||||
List<String> commandline = new ArrayList<>();
|
||||
commandline.add(JavaRuntime.getDefault().getBinary().toString());
|
||||
for (Map.Entry<Object, Object> entry : System.getProperties().entrySet()) {
|
||||
Object key = entry.getKey();
|
||||
if (key instanceof String && ((String) key).startsWith("hmcl.")) {
|
||||
commandline.add("-D" + key + "=" + entry.getValue());
|
||||
|
||||
try {
|
||||
for (String inputArgument : ManagementFactory.getRuntimeMXBean().getInputArguments()) {
|
||||
if (inputArgument.startsWith("-D") || inputArgument.startsWith("-X")) {
|
||||
commandline.add(inputArgument);
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
// ManagementFactory not available
|
||||
for (Map.Entry<Object, Object> entry : System.getProperties().entrySet()) {
|
||||
if (entry.getKey() instanceof String key && key.startsWith("hmcl.")) {
|
||||
commandline.add("-D" + key + "=" + entry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
commandline.add("-jar");
|
||||
commandline.add(jar.toAbsolutePath().toString());
|
||||
commandline.addAll(Arrays.asList(appArgs));
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.Month;
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
|
||||
/// April Fools' Day utilities.
|
||||
///
|
||||
/// This class provides methods to check if it is April Fools' Day or near April Fools' Day.
|
||||
/// It also provides a method to check if April Fools is enabled.
|
||||
///
|
||||
/// @author Glavo
|
||||
public final class AprilFools {
|
||||
|
||||
private static final boolean ENABLED;
|
||||
private static final boolean SHOW_APRIL_FOOLS_SETTINGS;
|
||||
|
||||
static {
|
||||
var date = LocalDate.now();
|
||||
|
||||
// Some countries/regions may oppose April Fools' Day for various reasons.
|
||||
// Therefore, we use a regional whitelist to avoid risks.
|
||||
// Currently, we have only listed a limited set of countries/regions for testing.
|
||||
// We will investigate more countries/regions in the future to expand this list.
|
||||
boolean supportedRegion = List.of(
|
||||
"CN", "TW", "HK", "MO", "JP", "KR", "VN", "SG", "MY",
|
||||
"ES", "DE", "FR", "GB", "RU", "UA", "US"
|
||||
).contains(LocaleUtils.SYSTEM_DEFAULT.getCountry());
|
||||
|
||||
boolean aprilFoolsMode;
|
||||
String value = System.getProperty("hmcl.april_fools", System.getenv("HMCL_APRIL_FOOLS"));
|
||||
if ("true".equalsIgnoreCase(value))
|
||||
aprilFoolsMode = true;
|
||||
else if ("false".equalsIgnoreCase(value) || !supportedRegion)
|
||||
aprilFoolsMode = false;
|
||||
else
|
||||
aprilFoolsMode = date.getMonth() == Month.APRIL && date.getDayOfMonth() == 1;
|
||||
|
||||
ENABLED = aprilFoolsMode && !config().isDisableAprilFools();
|
||||
SHOW_APRIL_FOOLS_SETTINGS = aprilFoolsMode || supportedRegion && date.getMonth() == Month.MARCH && date.getDayOfMonth() > 30;
|
||||
}
|
||||
|
||||
/// Whether April Fools settings should be shown.
|
||||
///
|
||||
/// This method returns true if April Fools settings should be shown.
|
||||
public static boolean isShowAprilFoolsSettings() {
|
||||
return SHOW_APRIL_FOOLS_SETTINGS;
|
||||
}
|
||||
|
||||
/// Whether April Fools is enabled.
|
||||
///
|
||||
/// This method returns true if April Fools is enabled.
|
||||
public static boolean isEnabled() {
|
||||
return ENABLED;
|
||||
}
|
||||
|
||||
private AprilFools() {
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -29,18 +30,21 @@ import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
/// @author Glavo
|
||||
public final class FileSaver extends Thread {
|
||||
|
||||
private static final Pair<Path, String> SHUTDOWN = Pair.pair(null, null);
|
||||
|
||||
private static final BlockingQueue<Pair<Path, String>> queue = new LinkedBlockingQueue<>();
|
||||
private static final BlockingQueue<Action> queue = new LinkedBlockingQueue<>();
|
||||
private static final AtomicBoolean running = new AtomicBoolean(false);
|
||||
private static final ReentrantLock runningLock = new ReentrantLock();
|
||||
private static volatile boolean shutdown = false;
|
||||
|
||||
private static void addAction(Action action) {
|
||||
queue.add(action);
|
||||
if (running.compareAndSet(false, true)) {
|
||||
new FileSaver().start();
|
||||
}
|
||||
}
|
||||
|
||||
private static void doSave(Map<Path, String> map) {
|
||||
for (Map.Entry<Path, String> entry : map.entrySet()) {
|
||||
saveSync(entry.getKey(), entry.getValue());
|
||||
@@ -53,10 +57,7 @@ public final class FileSaver extends Thread {
|
||||
|
||||
ShutdownHook.ensureInstalled();
|
||||
|
||||
queue.add(Pair.pair(file, content));
|
||||
if (running.compareAndSet(false, true)) {
|
||||
new FileSaver().start();
|
||||
}
|
||||
addAction(new DoSave(file, content));
|
||||
}
|
||||
|
||||
public static void saveSync(Path file, String content) {
|
||||
@@ -70,7 +71,17 @@ public final class FileSaver extends Thread {
|
||||
|
||||
public static void shutdown() {
|
||||
shutdown = true;
|
||||
queue.add(SHUTDOWN);
|
||||
queue.add(Shutdown.INSTANCE);
|
||||
}
|
||||
|
||||
/// Wait for all saves to complete.
|
||||
///
|
||||
/// This method should be called before the [#shutdown()] method.
|
||||
public static void waitForAllSaves() throws InterruptedException {
|
||||
assert !shutdown;
|
||||
Wait wait = new Wait();
|
||||
addAction(wait);
|
||||
wait.await();
|
||||
}
|
||||
|
||||
private FileSaver() {
|
||||
@@ -92,34 +103,46 @@ public final class FileSaver extends Thread {
|
||||
runningLock.lock();
|
||||
try {
|
||||
HashMap<Path, String> map = new HashMap<>();
|
||||
ArrayList<Pair<Path, String>> buffer = new ArrayList<>();
|
||||
ArrayList<Action> buffer = new ArrayList<>();
|
||||
ArrayList<Wait> waits = new ArrayList<>();
|
||||
|
||||
while (!stopped) {
|
||||
if (shutdown) {
|
||||
stopCurrentSaver();
|
||||
} else {
|
||||
Pair<Path, String> head = queue.poll(30, TimeUnit.SECONDS);
|
||||
if (head == null || head == SHUTDOWN) {
|
||||
stopCurrentSaver();
|
||||
} else {
|
||||
map.put(head.getKey(), head.getValue());
|
||||
Action head = queue.poll(30, TimeUnit.SECONDS);
|
||||
if (head instanceof DoSave save) {
|
||||
map.put(save.file(), save.content());
|
||||
//noinspection BusyWait
|
||||
Thread.sleep(200); // Waiting for more changes
|
||||
} else if (head instanceof Wait wait) {
|
||||
waits.add(wait);
|
||||
} else if (head == null || head instanceof Shutdown) {
|
||||
// Shutdown or timeout
|
||||
stopCurrentSaver();
|
||||
}
|
||||
}
|
||||
|
||||
while (queue.drainTo(buffer) > 0) {
|
||||
for (Pair<Path, String> pair : buffer) {
|
||||
if (pair == SHUTDOWN)
|
||||
for (Action action : buffer) {
|
||||
if (action instanceof DoSave save) {
|
||||
map.put(save.file(), save.content());
|
||||
} else if (action instanceof Wait wait) {
|
||||
waits.add(wait);
|
||||
} else if (action instanceof Shutdown) {
|
||||
stopCurrentSaver();
|
||||
else
|
||||
map.put(pair.getKey(), pair.getValue());
|
||||
}
|
||||
}
|
||||
buffer.clear();
|
||||
}
|
||||
|
||||
doSave(map);
|
||||
map.clear();
|
||||
|
||||
for (Wait wait : waits) {
|
||||
wait.countDown();
|
||||
}
|
||||
waits.clear();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
throw new AssertionError("This thread cannot be interrupted", e);
|
||||
@@ -128,6 +151,28 @@ public final class FileSaver extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
private sealed interface Action {
|
||||
}
|
||||
|
||||
private record DoSave(Path file, String content) implements Action {
|
||||
}
|
||||
|
||||
private static final class Wait implements Action {
|
||||
private final CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
public void await() throws InterruptedException {
|
||||
latch.await();
|
||||
}
|
||||
|
||||
public void countDown() {
|
||||
latch.countDown();
|
||||
}
|
||||
}
|
||||
|
||||
private enum Shutdown implements Action {
|
||||
INSTANCE
|
||||
}
|
||||
|
||||
private static final class ShutdownHook extends Thread {
|
||||
|
||||
static {
|
||||
@@ -144,9 +189,10 @@ public final class FileSaver extends Thread {
|
||||
runningLock.lock();
|
||||
try {
|
||||
HashMap<Path, String> map = new HashMap<>();
|
||||
for (Pair<Path, String> pair : queue) {
|
||||
if (pair != SHUTDOWN)
|
||||
map.put(pair.getKey(), pair.getValue());
|
||||
for (Action action : queue) {
|
||||
if (action instanceof DoSave save) {
|
||||
map.put(save.file(), save.content());
|
||||
}
|
||||
}
|
||||
doSave(map);
|
||||
} finally {
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.util;
|
||||
|
||||
import javafx.beans.value.WritableValue;
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
@@ -33,18 +34,20 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// @author Glavo
|
||||
public abstract class RemoteImageLoader {
|
||||
private final DownloadProvider downloadProvider;
|
||||
private final Map<URI, WeakReference<Image>> cache = new HashMap<>();
|
||||
private final Map<URI, List<WeakReference<WritableValue<Image>>>> pendingRequests = new HashMap<>();
|
||||
private final WeakHashMap<WritableValue<Image>, URI> reverseLookup = new WeakHashMap<>();
|
||||
|
||||
public RemoteImageLoader() {
|
||||
public RemoteImageLoader(DownloadProvider downloadProvider) {
|
||||
this.downloadProvider = downloadProvider;
|
||||
}
|
||||
|
||||
protected @Nullable Image getPlaceholder() {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected abstract @NotNull Task<Image> createLoadTask(@NotNull URI uri);
|
||||
protected abstract @NotNull Task<Image> createLoadTask(@NotNull List<URI> uris);
|
||||
|
||||
@FXThread
|
||||
public void load(@NotNull WritableValue<Image> writableValue, String url) {
|
||||
@@ -82,7 +85,7 @@ public abstract class RemoteImageLoader {
|
||||
}
|
||||
}
|
||||
|
||||
createLoadTask(uri).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
createLoadTask(downloadProvider.injectURLWithCandidates(url)).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
Image image;
|
||||
if (exception == null) {
|
||||
image = result;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// @author Glavo
|
||||
public final class Restarter {
|
||||
|
||||
/// Restart the current application.
|
||||
public static void restartSelf() throws IOException {
|
||||
LOG.info("Restarting HMCL");
|
||||
|
||||
Path thisJar = JarUtils.thisJarPath();
|
||||
if (thisJar == null) {
|
||||
throw new IOException("Failed to find current HMCL location");
|
||||
}
|
||||
|
||||
UpdateHandler.startJava(thisJar);
|
||||
}
|
||||
|
||||
private Restarter() {
|
||||
}
|
||||
}
|
||||
@@ -45,9 +45,9 @@
|
||||
"externalLink" : "https://github.com/jenkinsci/constant-pool-scanner"
|
||||
},
|
||||
{
|
||||
"title" : "OpenNBT",
|
||||
"subtitle" : "Copyright © 2013-2021 Steveice10.\nLicensed under the MIT License.",
|
||||
"externalLink" : "https://github.com/GeyserMC/OpenNBT"
|
||||
"title" : "HelloNBT",
|
||||
"subtitle" : "Copyright © 2026 Glavo.\nLicensed under the Apache 2.0 License.",
|
||||
"externalLink" : "https://github.com/Glavo/HelloNBT"
|
||||
},
|
||||
{
|
||||
"title" : "minecraft-jfx-skin",
|
||||
|
||||
@@ -113,28 +113,26 @@
|
||||
.memory-label {
|
||||
}
|
||||
|
||||
.memory-used {
|
||||
-fx-background-color: -monet-primary;
|
||||
.memory-status-bar {
|
||||
-fx-opacity: 1;
|
||||
}
|
||||
|
||||
.memory-used:disabled {
|
||||
.memory-status-bar:disabled {
|
||||
-fx-opacity: 0.4;
|
||||
}
|
||||
|
||||
.memory-allocate {
|
||||
-fx-background-color: -monet-primary-transparent-50;
|
||||
.memory-status-bar .track {
|
||||
-fx-background-color: -monet-secondary-container;
|
||||
-fx-background-radius: 2;
|
||||
-fx-background-insets: 0;
|
||||
}
|
||||
|
||||
.memory-allocate:disabled {
|
||||
-fx-opacity: 0.4;
|
||||
.memory-status-bar .memory-used {
|
||||
-fx-fill: -monet-primary;
|
||||
}
|
||||
|
||||
.memory-total {
|
||||
-fx-background-color: -monet-surface-container;
|
||||
}
|
||||
|
||||
.memory-total:disabled {
|
||||
-fx-opacity: 0.4;
|
||||
.memory-status-bar .memory-allocate {
|
||||
-fx-fill: -monet-primary-transparent-50;
|
||||
}
|
||||
|
||||
.update-label {
|
||||
@@ -527,7 +525,8 @@
|
||||
}
|
||||
|
||||
.jfx-dialog-layout {
|
||||
-fx-padding: 24.0px 24.0px 16.0px 24.0px;
|
||||
-fx-background-radius: 4px;
|
||||
-fx-padding: 24px 24px 16px 24px;
|
||||
-fx-background-color: -monet-surface-container-high;
|
||||
-fx-text-fill: -monet-on-surface;
|
||||
}
|
||||
@@ -570,6 +569,7 @@
|
||||
}
|
||||
|
||||
.task-executor-dialog-layout {
|
||||
-fx-background-radius: 4px;
|
||||
-fx-background-color: -monet-surface-container-high;
|
||||
-fx-text-fill: -monet-on-surface;
|
||||
}
|
||||
@@ -913,28 +913,22 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
|
||||
.jfx-progress-bar > .track {
|
||||
-fx-background-color: -monet-secondary-container;
|
||||
}
|
||||
|
||||
.jfx-progress-bar > .bar,
|
||||
.jfx-progress-bar:indeterminate > .bar{
|
||||
.jfx-progress-bar > .bar {
|
||||
-fx-background-color: -monet-primary;
|
||||
-fx-padding: 1.5;
|
||||
}
|
||||
|
||||
.jfx-progress-bar > .secondary-bar,
|
||||
.jfx-progress-bar:indeterminate > .secondary-bar {
|
||||
-fx-background-color: -monet-secondary;
|
||||
-fx-padding: 2;
|
||||
}
|
||||
|
||||
.jfx-progress-bar > .track,
|
||||
.jfx-progress-bar > .bar {
|
||||
-fx-background-radius: 0;
|
||||
-fx-background-radius: 2;
|
||||
-fx-background-insets: 0;
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* JFX Textfield *
|
||||
@@ -1457,20 +1451,8 @@
|
||||
-fx-stroke: -monet-primary-container;
|
||||
}
|
||||
|
||||
.jfx-spinner:determinate .percentage {
|
||||
-fx-fill: -monet-on-surface-variant;
|
||||
}
|
||||
|
||||
.first-spinner {
|
||||
-jfx-radius: 20;
|
||||
}
|
||||
|
||||
.first-spinner .arc {
|
||||
-fx-stroke-width: 5.0;
|
||||
}
|
||||
|
||||
.small-spinner {
|
||||
-jfx-radius: 10;
|
||||
-jfx-radius: 9;
|
||||
}
|
||||
|
||||
.small-spinner .arc {
|
||||
@@ -1478,59 +1460,7 @@
|
||||
}
|
||||
|
||||
.small-spinner-pane .jfx-spinner {
|
||||
-jfx-radius: 10;
|
||||
}
|
||||
|
||||
.small-spinner-pane .jfx-spinner .arc {
|
||||
-fx-stroke-width: 3.0;
|
||||
}
|
||||
|
||||
.second-spinner {
|
||||
-jfx-radius: 30;
|
||||
}
|
||||
|
||||
.second-spinner .arc {
|
||||
-fx-stroke-width: 5.0;
|
||||
}
|
||||
|
||||
.third-spinner {
|
||||
-jfx-radius: 40;
|
||||
}
|
||||
|
||||
.third-spinner .arc {
|
||||
-fx-stroke-width: 5.0;
|
||||
}
|
||||
|
||||
.fourth-spinner {
|
||||
-jfx-radius: 50;
|
||||
}
|
||||
|
||||
.fourth-spinner .arc {
|
||||
-fx-stroke-width: 5.0;
|
||||
}
|
||||
|
||||
.fifth-spinner {
|
||||
-jfx-radius: 60;
|
||||
}
|
||||
|
||||
.fifth-spinner .arc {
|
||||
-fx-stroke-width: 5.0;
|
||||
}
|
||||
|
||||
.sixth-spinner {
|
||||
-jfx-radius: 70;
|
||||
}
|
||||
|
||||
.sixth-spinner .arc {
|
||||
-fx-stroke-width: 5.0;
|
||||
}
|
||||
|
||||
.seventh-spinner {
|
||||
-jfx-radius: 80;
|
||||
}
|
||||
|
||||
.seventh-spinner .arc {
|
||||
-fx-stroke-width: 5.0;
|
||||
-jfx-radius: 9;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
@@ -1993,4 +1923,4 @@
|
||||
|
||||
.line-toggle-button .jfx-toggle-button {
|
||||
-fx-padding: 0;
|
||||
}
|
||||
}
|
||||
@@ -334,6 +334,10 @@ curse.category.4550=Quests
|
||||
curse.category.4555=World Gen
|
||||
curse.category.4552=Scripts
|
||||
|
||||
curse.category.6553=Realistic
|
||||
curse.category.6554=Fantasy
|
||||
curse.category.6555=Vanilla
|
||||
|
||||
curse.sort.author=Author
|
||||
curse.sort.date_created=Date Created
|
||||
curse.sort.last_updated=Last Updated
|
||||
@@ -794,7 +798,7 @@ launch.advice=%s Do you still want to continue to launch?
|
||||
launch.advice.multi=The following problems were detected:\n\n%s\n\nThese problems may prevent you from launching the game or affect gaming experience.\nDo you still want to continue to launch?
|
||||
launch.advice.java.auto=The current Java version is not compatible with the instance.\n\nClick "Yes" to automatically choose the most compatible Java version. Or, you can navigate to "Global/Instance-specific Settings → Java" to choose one yourself.
|
||||
launch.advice.java.modded_java_7=Minecraft 1.7.2 and previous versions require Java 7 or earlier.
|
||||
launch.advice.cleanroom=Cleanroom can only be run on Java 21 or later. Please use Java 21 or later versions.
|
||||
launch.advice.cleanroom=Cleanroom %2$s can only be run on Java %1$s or later. Please use Java %1$s or later versions.
|
||||
launch.advice.corrected=We have resolved the Java problem. If you still want to use your choice of Java version, you can disable "Do not check JVM compatibility" in "Global/Instance-specific Settings → Advanced Settings".
|
||||
launch.advice.uncorrected=If you still want to use your choice of Java version, you can disable "Do not check JVM compatibility" in "Global/Instance-specific Settings → Advanced Settings".
|
||||
launch.advice.different_platform=The 64-bit Java version is recommended for your device, but you have installed a 32-bit one.
|
||||
@@ -840,6 +844,8 @@ launcher.agreement=ToS and EULA
|
||||
launcher.agreement.accept=Accept
|
||||
launcher.agreement.decline=Decline
|
||||
launcher.agreement.hint=You must agree to the EULA to use this software.
|
||||
launcher.april_fools.switch_lzh=HMCL Launcher now supports Classical Chinese, do you want to switch to Classical Chinese mode?\nYou can switch back to English in "置設 → 貫用 → 文".
|
||||
launcher.april_fools.switch_lzh.confirm=Confirm switching to Classical Chinese mode?\nClick "OK" to exit HMCL and restart it in Classical Chinese mode; click "Cancel" to keep the current language and enter HMCL homepage.
|
||||
launcher.background=Background Image
|
||||
launcher.background.choose=Choose background image
|
||||
launcher.background.classic=Classic
|
||||
@@ -1375,8 +1381,9 @@ settings.advanced.renderer.zink.desc=Vulkan (Best performance, poor compatibilit
|
||||
settings.advanced.server_ip=Server Address
|
||||
settings.advanced.server_ip.prompt=Automatically join after launching the game
|
||||
settings.advanced.unsupported_system_options=Settings not applicable to the current system
|
||||
settings.advanced.use_native_glfw=[Linux/FreeBSD Only] Use System GLFW
|
||||
settings.advanced.use_native_openal=[Linux/FreeBSD Only] Use System OpenAL
|
||||
settings.advanced.use_native_glfw=Use System GLFW
|
||||
settings.advanced.use_native_openal=Use System OpenAL
|
||||
settings.advanced.linux_freebsd_only=Linux/FreeBSD Only
|
||||
settings.advanced.workaround=Workaround
|
||||
settings.advanced.workaround.warning=Workaround options are intended only for advanced users. Tweaking with these options may crash the game. Unless you know what you are doing, please do not edit these options.
|
||||
settings.advanced.wrapper_launcher=Wrapper Command
|
||||
@@ -1417,6 +1424,7 @@ settings.launcher.brightness.dark=Dark Mode
|
||||
settings.launcher.brightness.light=Light Mode
|
||||
settings.launcher.common_path.tooltip=HMCL will put all game assets and dependencies here. If there are existing libraries in the game directory, then HMCL will prefer to use them first.
|
||||
settings.launcher.debug=Debug
|
||||
settings.launcher.disable_april_fools=Do not enable April Fools features
|
||||
settings.launcher.disable_auto_game_options=Do not switch game language
|
||||
settings.launcher.download=Download
|
||||
settings.launcher.download.threads=Threads
|
||||
|
||||
@@ -1375,8 +1375,9 @@ settings.advanced.renderer.zink.desc=Vulkan (أفضل أداء، توافق ضع
|
||||
settings.advanced.server_ip=عنوان الخادم
|
||||
settings.advanced.server_ip.prompt=الانضمام تلقائياً بعد تشغيل اللعبة
|
||||
settings.advanced.unsupported_system_options=إعدادات غير مطبّقة على النظام الحالي
|
||||
settings.advanced.use_native_glfw=[Linux/FreeBSD فقط] استخدام GLFW النظام
|
||||
settings.advanced.use_native_openal=[Linux/FreeBSD فقط] استخدام OpenAL النظام
|
||||
settings.advanced.use_native_glfw=استخدام GLFW النظام
|
||||
settings.advanced.use_native_openal=استخدام OpenAL النظام
|
||||
settings.advanced.linux_freebsd_only=Linux/FreeBSD فقط
|
||||
settings.advanced.workaround=حلول بديلة
|
||||
settings.advanced.workaround.warning=خيارات الحلول البديلة مخصصة للمستخدمين المتقدمين فقط. قد يتسبب تعديل هذه الخيارات في تعطل اللعبة. يُرجى عدم تعديل هذه الخيارات إلا إذا كنت تعرف ما تفعله.
|
||||
settings.advanced.wrapper_launcher=أمر التغليف
|
||||
|
||||
@@ -1279,8 +1279,9 @@ settings.advanced.renderer.zink.desc=Vulkan (Máximo rendimiento, baja compatibi
|
||||
settings.advanced.server_ip=Dirección del servidor
|
||||
settings.advanced.server_ip.prompt=Entrar automáticamente después de ejecutar el juego
|
||||
settings.advanced.unsupported_system_options=Configuración no aplicable al sistema actual
|
||||
settings.advanced.use_native_glfw=[Sólo Linux/FreeBSD] Utilizar GLFW nativo
|
||||
settings.advanced.use_native_openal=[Sólo Linux/FreeBSD] Utilizar OpenAL nativo
|
||||
settings.advanced.use_native_glfw=Utilizar GLFW nativo
|
||||
settings.advanced.use_native_openal=Utilizar OpenAL nativo
|
||||
settings.advanced.linux_freebsd_only=Sólo Linux/FreeBSD
|
||||
settings.advanced.workaround=Métodos alternativos
|
||||
settings.advanced.workaround.warning=Éstas opciones están pensadas sólo para usuarios expertos. Jugar con estas opciones puede romper el juego. A menos que sepas lo que estás haciendo, por favor no modifiques estas opciones.
|
||||
settings.advanced.wrapper_launcher=Comando Wrapper
|
||||
|
||||
@@ -802,8 +802,9 @@ settings.advanced.renderer.zink.desc=Vulkan (最高パフォーマンス、低
|
||||
settings.advanced.server_ip=サーバーアドレス
|
||||
settings.advanced.server_ip.prompt=ゲームの起動時にサーバーに参加する
|
||||
settings.advanced.unsupported_system_options=サポートされていないシステムオプション
|
||||
settings.advanced.use_native_glfw=[Linux/FreeBSDのみ]システムGLFWを使用する
|
||||
settings.advanced.use_native_openal=[Linux/FreeBSDのみ]システムOpenALを使用する
|
||||
settings.advanced.use_native_glfw=システムGLFWを使用する
|
||||
settings.advanced.use_native_openal=システムOpenALを使用する
|
||||
settings.advanced.linux_freebsd_only=Linux/FreeBSDのみ
|
||||
settings.advanced.workaround=デバッグ用オプション
|
||||
settings.advanced.workaround.warning=デバッグオプションはプロフェッショナルのみ使用可能です。デバッグオプションにより、ゲームが起動しない場合があります。これらのオプションは、ご自分が何をしているのかが分からない限り、変更しないでください。
|
||||
settings.advanced.wrapper_launcher=パッキングオーダー
|
||||
|
||||
@@ -975,7 +975,10 @@ repositories.chooser=闕 JavaFX。HMCL 須之以行。\n擊「善」而引 JavaF
|
||||
repositories.chooser.title=擇 JavaFX 之源
|
||||
|
||||
resourcepack=資囊
|
||||
resourcepack.add=增資囊
|
||||
resourcepack.download=引資囊
|
||||
resourcepack.download.title=引資囊 - %1s
|
||||
resourcepack.manage=司資囊
|
||||
|
||||
reveal.in_file_manager=覽於理案臺
|
||||
|
||||
@@ -1090,8 +1093,9 @@ settings.advanced.renderer.zink.desc=Vulkan (效至,所適差)
|
||||
settings.advanced.server_ip=伺服器之址
|
||||
settings.advanced.server_ip.prompt=本之址,啟訖徑入是伺服器
|
||||
settings.advanced.unsupported_system_options=不逮今之械網所選
|
||||
settings.advanced.use_native_glfw=[惟磷若/FreeBSD] 用 GLFW
|
||||
settings.advanced.use_native_openal=[惟磷若/FreeBSD] 用開聲庫
|
||||
settings.advanced.use_native_glfw=用 GLFW
|
||||
settings.advanced.use_native_openal=用開聲庫
|
||||
settings.advanced.linux_freebsd_only=惟磷若/FreeBSD
|
||||
settings.advanced.workaround=勘誤置設
|
||||
settings.advanced.workaround.warning=勘誤置設者,蓋專者之所行也。苟妄改之,戲或弗啟。微其之知,無復改焉!
|
||||
settings.advanced.wrapper_launcher=包裝之令
|
||||
@@ -1140,7 +1144,7 @@ settings.launcher.font.anti_aliasing.auto=自調
|
||||
settings.launcher.font.anti_aliasing.gray=灰階
|
||||
settings.launcher.font.anti_aliasing.lcd=子像素
|
||||
settings.launcher.general=貫用
|
||||
settings.launcher.language=文 (複啟而效)
|
||||
settings.launcher.language=文
|
||||
settings.launcher.launcher_log.export=錄出啟者之誌
|
||||
settings.launcher.launcher_log.export.failed=錄誌出而未成
|
||||
settings.launcher.launcher_log.export.success=誌既存於「%s」
|
||||
@@ -1193,6 +1197,8 @@ sponsor.hmcl=Hello Minecraft! Launcher,乃一免費,自由,開源之礦藝
|
||||
system.architecture=大構
|
||||
system.operating_system=械網
|
||||
|
||||
terracotta=眾戲
|
||||
|
||||
unofficial.hint=子方用非官築之 HMCL。余無能證其安,請君自辨。
|
||||
|
||||
update=啟者迭更
|
||||
@@ -1210,6 +1216,8 @@ update.channel.nightly.hint=君所用之 HMCL 乃預版也。預版之更迭常
|
||||
update.channel.nightly.title=預版告
|
||||
update.channel.stable=定版
|
||||
update.checking=甫察其新
|
||||
update.disable_auto_show_update_dialog=弗自顯迭更窗
|
||||
update.disable_auto_show_update_dialog.subtitle=啟此,HMCL 將弗自顯迭更窗。
|
||||
update.failed=迭更未成
|
||||
update.found=尋得新者
|
||||
update.newest_version=至新:%s
|
||||
@@ -1219,6 +1227,8 @@ update.note=开發版與預版,虽增功、修阙,然亦恐生他患。
|
||||
update.latest=當版至新
|
||||
update.no_browser=無啟瀏覽器。址固鈔訖,君可自粘之以啟頁。
|
||||
update.tooltip=迭更
|
||||
update.preview=先覽 HMCL 之版
|
||||
update.preview.subtitle=啟此,君可先得 HMCL 之新,試之於未行。
|
||||
|
||||
version=戲
|
||||
version.name=戲例之名
|
||||
|
||||
@@ -1270,8 +1270,9 @@ settings.advanced.renderer.zink.desc=Vulkan (Лучшая производите
|
||||
settings.advanced.server_ip=Адрес сервера
|
||||
settings.advanced.server_ip.prompt=Присоединяться к серверу при запуске игры
|
||||
settings.advanced.unsupported_system_options=Настройки, не применимые к текущей системе
|
||||
settings.advanced.use_native_glfw=[Только для Linux/FreeBSD] Использовать системный GLFW
|
||||
settings.advanced.use_native_openal=[Только для Linux/FreeBSD] Использовать системный OpenAL
|
||||
settings.advanced.use_native_glfw=Использовать системный GLFW
|
||||
settings.advanced.use_native_openal=Использовать системный OpenAL
|
||||
settings.advanced.linux_freebsd_only=Только для Linux/FreeBSD
|
||||
settings.advanced.workaround=Обходные пути
|
||||
settings.advanced.workaround.warning=Варианты обхода предназначены только для опытных пользователей. Изменение этих параметров может привести к вылету игры. Если не знаете, что делаете, то не изменяйте эти параметры.
|
||||
settings.advanced.wrapper_launcher=Команда-оболочка
|
||||
|
||||
@@ -1217,8 +1217,9 @@ settings.advanced.renderer.zink.desc=Vulkan (Найкраща продуктив
|
||||
settings.advanced.server_ip=Адреса сервера
|
||||
settings.advanced.server_ip.prompt=Автоматично приєднатися після запуску гри
|
||||
settings.advanced.unsupported_system_options=Налаштування, що не застосовуються до поточної системи
|
||||
settings.advanced.use_native_glfw=[Лише Linux/FreeBSD] Використовувати системний GLFW
|
||||
settings.advanced.use_native_openal=[Лише Linux/FreeBSD] Використовувати системний OpenAL
|
||||
settings.advanced.use_native_glfw=Використовувати системний GLFW
|
||||
settings.advanced.use_native_openal=Використовувати системний OpenAL
|
||||
settings.advanced.linux_freebsd_only=Лише Linux/FreeBSD
|
||||
settings.advanced.workaround=Обхідні шляхи
|
||||
settings.advanced.workaround.warning=Опції обхідних шляхів призначені лише для досвідчених користувачів. Налаштування цих опцій може призвести до збою гри. Якщо ви не знаєте, що робите, не редагуйте ці опції.
|
||||
settings.advanced.wrapper_launcher=Команда обгортки
|
||||
|
||||
@@ -330,6 +330,10 @@ curse.category.4550=任務
|
||||
curse.category.4555=世界生成
|
||||
curse.category.4552=指令碼
|
||||
|
||||
curse.category.6553=寫實
|
||||
curse.category.6554=幻想
|
||||
curse.category.6555=原生
|
||||
|
||||
curse.sort.author=作者
|
||||
curse.sort.date_created=建立日期
|
||||
curse.sort.last_updated=最近更新
|
||||
@@ -598,7 +602,7 @@ launch.advice=%s 是否繼續啟動?
|
||||
launch.advice.multi=檢測到以下問題:\n\n%s\n\n這些問題可能導致遊戲無法正常啟動或影響遊戲體驗,是否繼續啟動?
|
||||
launch.advice.java.auto=目前選取的 Java 版本不滿足遊戲要求,是否自動選取合適的 Java 版本?\n或者你可以到「(全域/實例特定) 遊戲設定 → 遊戲 Java」中選取一個合適的 Java 版本。
|
||||
launch.advice.java.modded_java_7=Minecraft 1.7.2 及更低版本需要 Java 7 及更低版本。
|
||||
launch.advice.cleanroom=Cleanroom 僅支援 Java 21 或更高版本。請使用 Java 21 或最新版本。
|
||||
launch.advice.cleanroom=Cleanroom %2$s 只能在 Java %1$s 或更高版本上運行。請使用 Java %1$s 或最新版本。
|
||||
launch.advice.corrected=我們已經修正了 Java 版本問題。如果你確實希望使用你自訂的 Java,你可以在「(全域/實例特定) 遊戲設定 → 進階設定」中往下滑,開啟「不檢查 Java 虛擬機與遊戲的相容性」。
|
||||
launch.advice.uncorrected=如果你確實希望使用你自訂的 Java,你可以在「(全域/實例特定) 遊戲設定 → 進階設定」中往下滑,開啟「不檢查 Java 虛擬機與遊戲的相容性」。
|
||||
launch.advice.different_platform=你正在使用 32 位元 Java 啟動遊戲。建議更換至 64 位元 Java。
|
||||
@@ -644,6 +648,8 @@ launcher.agreement=使用者協議與免責宣告
|
||||
launcher.agreement.accept=同意
|
||||
launcher.agreement.decline=拒絕
|
||||
launcher.agreement.hint=同意本軟體的使用者協議與免責宣告以使用本軟體。
|
||||
launcher.april_fools.switch_lzh=HMCL 啟動器現已支援文言文,我們誠邀你參與體驗。是否要將介面語言切換至中文 (文言)?\n在切換語言後,你可以在「置設 → 貫用 → 文」中將語言切換回中文 (繁體)。
|
||||
launcher.april_fools.switch_lzh.confirm=確認切換語言?\n點擊「確定」後 HMCL 將自動重新啟動並將介面語言切換至中文 (文言);點擊「取消」將繼續使用中文 (繁體) 進入 HMCL 主頁。
|
||||
launcher.background=背景圖片
|
||||
launcher.background.choose=選取背景圖片
|
||||
launcher.background.classic=經典
|
||||
@@ -1162,8 +1168,9 @@ settings.advanced.renderer.zink.desc=Vulkan (效能最好,相容性較差)
|
||||
settings.advanced.server_ip=伺服器位址
|
||||
settings.advanced.server_ip.prompt=預設,啟動遊戲後直接進入對應伺服器
|
||||
settings.advanced.unsupported_system_options=不適用於目前系統的選項
|
||||
settings.advanced.use_native_glfw=[僅限 Linux/FreeBSD] 使用系統 GLFW
|
||||
settings.advanced.use_native_openal=[僅限 Linux/FreeBSD] 使用系統 OpenAL
|
||||
settings.advanced.use_native_glfw=使用系統 GLFW
|
||||
settings.advanced.use_native_openal=使用系統 OpenAL
|
||||
settings.advanced.linux_freebsd_only=僅限 Linux/FreeBSD
|
||||
settings.advanced.workaround=除錯選項
|
||||
settings.advanced.workaround.warning=除錯選項僅提供給專業玩家使用。修改除錯選項可能會導致遊戲無法啟動。除非你知道你在做什麼,否則請不要修改這些選項。
|
||||
settings.advanced.wrapper_launcher=前置指令
|
||||
@@ -1202,6 +1209,7 @@ settings.launcher.brightness.dark=深色模式
|
||||
settings.launcher.brightness.light=淺色模式
|
||||
settings.launcher.common_path.tooltip=啟動器將所有遊戲資源及相依元件庫檔案放於此集中管理。如果遊戲目錄內有現成的將不會使用公共庫檔案。
|
||||
settings.launcher.debug=除錯
|
||||
settings.launcher.disable_april_fools=不啟用愚人節功能
|
||||
settings.launcher.disable_auto_game_options=不自動切換遊戲語言
|
||||
settings.launcher.download=下載
|
||||
settings.launcher.download.threads=執行緒數
|
||||
|
||||
@@ -332,6 +332,10 @@ curse.category.4550=任务
|
||||
curse.category.4555=世界生成
|
||||
curse.category.4552=脚本
|
||||
|
||||
curse.category.6553=写实
|
||||
curse.category.6554=幻想
|
||||
curse.category.6555=原生
|
||||
|
||||
curse.sort.author=作者
|
||||
curse.sort.date_created=创建日期
|
||||
curse.sort.last_updated=最近更新
|
||||
@@ -603,7 +607,7 @@ launch.advice=%s 是否继续启动?
|
||||
launch.advice.multi=检测到以下问题:\n\n%s\n\n这些问题可能导致游戏无法正常启动或影响游戏体验,是否继续启动?\n你可以点击右上角帮助按钮进行求助。
|
||||
launch.advice.java.auto=当前选择的 Java 版本不满足游戏要求。\n点击“是”即可由 HMCL 来自动选取合适的 Java 版本。\n或者你可以在“(全局/实例特定) 游戏设置 → 游戏 Java”中选择一个合适的 Java 版本。
|
||||
launch.advice.java.modded_java_7=Minecraft 1.7.2 及更低版本需要 Java 7 及更低版本。
|
||||
launch.advice.cleanroom=Cleanroom 只能在 Java 21 或更高版本上运行。请使用 Java 21 或最新版本。
|
||||
launch.advice.cleanroom=Cleanroom %2$s 只能在 Java %1$s 或更高版本上运行。请使用 Java %1$s 或最新版本。
|
||||
launch.advice.corrected=我们已经修复了 Java 版本问题。如果你确实希望使用你自定义的 Java,你可以在“(全局/实例特定) 游戏设置 → 高级设置”中往下滑,启用“不检查 Java 虚拟机与游戏的兼容性”。
|
||||
launch.advice.uncorrected=如果你确实希望使用你自定义的 Java,你可以在“(全局/实例特定) 游戏设置 → 高级设置”中往下滑,启用“不检查 Java 虚拟机与游戏的兼容性”。
|
||||
launch.advice.different_platform=你正在使用 32 位 Java 启动游戏。建议更换至 64 位 Java。
|
||||
@@ -649,6 +653,8 @@ launcher.agreement=用户协议与免责声明
|
||||
launcher.agreement.accept=同意
|
||||
launcher.agreement.decline=拒绝
|
||||
launcher.agreement.hint=同意本软件的用户协议与免责声明以使用本软件。
|
||||
launcher.april_fools.switch_lzh=HMCL 启动器现已支持文言文,我们诚邀你参与体验。是否要将界面语言切换至中文 (文言)?\n在切换语言后,你可以在“置設 → 貫用 → 文”中将语言切换回中文 (简体)。
|
||||
launcher.april_fools.switch_lzh.confirm=确认切换语言?\n点击“确定”后 HMCL 将自动重启并将界面语言切换至中文 (文言);点击“取消”将继续使用中文 (简体) 进入 HMCL 主页。
|
||||
launcher.background=背景图片
|
||||
launcher.background.choose=选择背景图片
|
||||
launcher.background.classic=经典
|
||||
@@ -1167,8 +1173,9 @@ settings.advanced.renderer.zink.desc=Vulkan (性能最好,兼容性较差)
|
||||
settings.advanced.server_ip=服务器地址
|
||||
settings.advanced.server_ip.prompt=默认,启动游戏后可以直接进入对应服务器
|
||||
settings.advanced.unsupported_system_options=不适用于当前系统的选项
|
||||
settings.advanced.use_native_glfw=[仅 Linux/FreeBSD] 使用系统 GLFW
|
||||
settings.advanced.use_native_openal=[仅 Linux/FreeBSD] 使用系统 OpenAL
|
||||
settings.advanced.use_native_glfw=使用系统 GLFW
|
||||
settings.advanced.use_native_openal=使用系统 OpenAL
|
||||
settings.advanced.linux_freebsd_only=仅 Linux/FreeBSD
|
||||
settings.advanced.workaround=调试选项
|
||||
settings.advanced.workaround.warning=调试选项仅提供给专业玩家使用。调试选项可能会导致游戏无法启动。除非你知道你在做什么,否则请不要修改这些选项!
|
||||
settings.advanced.wrapper_launcher=包装命令
|
||||
@@ -1207,6 +1214,7 @@ settings.launcher.brightness.dark=深色模式
|
||||
settings.launcher.brightness.light=浅色模式
|
||||
settings.launcher.common_path.tooltip=启动器将所有游戏资源及依赖库文件存放于此集中管理。如果游戏文件夹内有现成的将不会使用公共库文件。
|
||||
settings.launcher.debug=调试
|
||||
settings.launcher.disable_april_fools=不启用愚人节功能
|
||||
settings.launcher.disable_auto_game_options=不自动切换游戏语言
|
||||
settings.launcher.download=下载
|
||||
settings.launcher.download.threads=线程数
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
|
||||
@@ -18,14 +18,15 @@ dependencies {
|
||||
api(libs.gson)
|
||||
api(libs.toml)
|
||||
api(libs.xz)
|
||||
api(libs.lz4)
|
||||
api(libs.fx.gson)
|
||||
api(libs.constant.pool.scanner)
|
||||
api(libs.opennbt)
|
||||
api(libs.nanohttpd)
|
||||
api(libs.jsoup)
|
||||
api(libs.chardet)
|
||||
api(libs.jna)
|
||||
api(libs.pci.ids)
|
||||
api(libs.hello.nbt)
|
||||
|
||||
compileOnlyApi(libs.jetbrains.annotations)
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
@@ -57,6 +56,7 @@ public final class BMCLAPIDownloadProvider implements DownloadProvider {
|
||||
private final QuiltVersionList quilt;
|
||||
private final QuiltAPIVersionList quiltApi;
|
||||
private final List<Pair<String, String>> replacement;
|
||||
private final List<Pair<String, String>> fallbackReplacement;
|
||||
|
||||
public BMCLAPIDownloadProvider(String apiRoot) {
|
||||
this.apiRoot = apiRoot;
|
||||
@@ -73,7 +73,7 @@ public final class BMCLAPIDownloadProvider implements DownloadProvider {
|
||||
this.legacyFabric = new LegacyFabricVersionList(this);
|
||||
this.legacyFabricApi = new LegacyFabricAPIVersionList(this);
|
||||
|
||||
this.replacement = Arrays.asList(
|
||||
this.replacement = List.of(
|
||||
pair("https://bmclapi2.bangbang93.com", apiRoot),
|
||||
pair("https://launchermeta.mojang.com", apiRoot),
|
||||
pair("https://piston-meta.mojang.com", apiRoot),
|
||||
@@ -93,7 +93,10 @@ public final class BMCLAPIDownloadProvider implements DownloadProvider {
|
||||
pair("https://repo.maven.apache.org/maven2", "https://mirrors.cloud.tencent.com/nexus/repository/maven-public"),
|
||||
pair("https://hmcl.glavo.site/metadata/cleanroom", "https://alist.8mi.tech/d/mirror/HMCL-Metadata/Auto/cleanroom"),
|
||||
pair("https://hmcl.glavo.site/metadata/fmllibs", "https://alist.8mi.tech/d/mirror/HMCL-Metadata/Auto/fmllibs"),
|
||||
pair("https://zkitefly.github.io/unlisted-versions-of-minecraft", "https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto"),
|
||||
pair("https://zkitefly.github.io/unlisted-versions-of-minecraft", "https://alist.8mi.tech/d/mirror/unlisted-versions-of-minecraft/Auto")
|
||||
);
|
||||
|
||||
this.fallbackReplacement = List.of(
|
||||
// https://github.com/mcmod-info-mirror/mcim-rust-api
|
||||
pair("https://api.modrinth.com", "https://mod.mcimirror.top/modrinth"),
|
||||
pair("https://cdn.modrinth.com", "https://mod.mcimirror.top"),
|
||||
@@ -135,17 +138,38 @@ public final class BMCLAPIDownloadProvider implements DownloadProvider {
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public String injectURL(String baseURL) {
|
||||
private static String injectURL(List<Pair<String, String>> replacement, String baseURL) {
|
||||
for (Pair<String, String> pair : replacement) {
|
||||
if (baseURL.startsWith(pair.getKey())) {
|
||||
return pair.getValue() + baseURL.substring(pair.getKey().length());
|
||||
}
|
||||
}
|
||||
|
||||
return baseURL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String injectURL(String baseURL) {
|
||||
return injectURL(replacement, baseURL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<URI> injectURLWithCandidates(String baseURL) {
|
||||
String injected = injectURL(replacement, baseURL);
|
||||
if (injected.equals(baseURL)) {
|
||||
String fallbackInjected = injectURL(fallbackReplacement, baseURL);
|
||||
if (fallbackInjected.equals(baseURL)) {
|
||||
return List.of(NetworkUtils.toURI(baseURL));
|
||||
} else {
|
||||
return List.of(
|
||||
NetworkUtils.toURI(baseURL),
|
||||
NetworkUtils.toURI(fallbackInjected)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return List.of(NetworkUtils.toURI(injected));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getConcurrency() {
|
||||
return Math.max(Runtime.getRuntime().availableProcessors() * 2, 6);
|
||||
|
||||
@@ -42,7 +42,7 @@ public class FabricAPIVersionList extends VersionList<FabricAPIRemoteVersion> {
|
||||
@Override
|
||||
public Task<?> refreshAsync() {
|
||||
return Task.runAsync(() -> {
|
||||
for (RemoteMod.Version modVersion : Lang.toIterable(ModrinthRemoteModRepository.MODS.getRemoteVersionsById("P7dR8mSH"))) {
|
||||
for (RemoteMod.Version modVersion : Lang.toIterable(ModrinthRemoteModRepository.MODS.getRemoteVersionsById(downloadProvider, "P7dR8mSH"))) {
|
||||
for (String gameVersion : modVersion.getGameVersions()) {
|
||||
versions.put(gameVersion, new FabricAPIRemoteVersion(gameVersion, modVersion.getVersion(), modVersion.getName(), modVersion.getDatePublished(), modVersion,
|
||||
Collections.singletonList(modVersion.getFile().getUrl())));
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ public class LegacyFabricAPIVersionList extends VersionList<LegacyFabricAPIRemot
|
||||
@Override
|
||||
public Task<?> refreshAsync() {
|
||||
return Task.runAsync(() -> {
|
||||
for (RemoteMod.Version modVersion : Lang.toIterable(ModrinthRemoteModRepository.MODS.getRemoteVersionsById("legacy-fabric-api"))) {
|
||||
for (RemoteMod.Version modVersion : Lang.toIterable(ModrinthRemoteModRepository.MODS.getRemoteVersionsById(downloadProvider, "legacy-fabric-api"))) {
|
||||
for (String gameVersion : modVersion.getGameVersions()) {
|
||||
versions.put(gameVersion, new LegacyFabricAPIRemoteVersion(gameVersion, modVersion.getVersion(), modVersion.getName(), modVersion.getDatePublished(), modVersion,
|
||||
Collections.singletonList(modVersion.getFile().getUrl())));
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.download.neoforge;
|
||||
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
|
||||
+17
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.download.neoforge;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
|
||||
+17
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.download.neoforge;
|
||||
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
|
||||
@@ -42,7 +42,7 @@ public class QuiltAPIVersionList extends VersionList<QuiltAPIRemoteVersion> {
|
||||
@Override
|
||||
public Task<?> refreshAsync() {
|
||||
return Task.runAsync(() -> {
|
||||
for (RemoteMod.Version modVersion : Lang.toIterable(ModrinthRemoteModRepository.MODS.getRemoteVersionsById("qsl"))) {
|
||||
for (RemoteMod.Version modVersion : Lang.toIterable(ModrinthRemoteModRepository.MODS.getRemoteVersionsById(downloadProvider, "qsl"))) {
|
||||
for (String gameVersion : modVersion.getGameVersions()) {
|
||||
versions.put(gameVersion, new QuiltAPIRemoteVersion(gameVersion, modVersion.getVersion(), modVersion.getName(), modVersion.getDatePublished(), modVersion,
|
||||
Collections.singletonList(modVersion.getFile().getUrl())));
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.sun.tools.attach.AttachNotSupportedException;
|
||||
|
||||
@@ -17,11 +17,13 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -35,7 +37,7 @@ public record GameJavaVersion(String component, int majorVersion) {
|
||||
public static final GameJavaVersion JAVA_16 = new GameJavaVersion("java-runtime-alpha", 16);
|
||||
public static final GameJavaVersion JAVA_8 = new GameJavaVersion("jre-legacy", 8);
|
||||
|
||||
public static final GameJavaVersion LATEST = JAVA_21;
|
||||
public static final GameJavaVersion LATEST = JAVA_25;
|
||||
|
||||
public static GameJavaVersion getMinimumJavaVersion(GameVersionNumber gameVersion) {
|
||||
if (gameVersion.compareTo("26.1") >= 0)
|
||||
@@ -51,6 +53,14 @@ public record GameJavaVersion(String component, int majorVersion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public static GameJavaVersion getCleanroomJavaVersion(String cleanroomVersion) {
|
||||
VersionNumber versionNumber = VersionNumber.asVersion(StringUtils.removeSuffix(cleanroomVersion, "-alpha"));
|
||||
if (versionNumber.compareTo("0.5.0") >= 0)
|
||||
return JAVA_25;
|
||||
else
|
||||
return JAVA_21;
|
||||
}
|
||||
|
||||
public static GameJavaVersion get(int major) {
|
||||
return switch (major) {
|
||||
case 8 -> JAVA_8;
|
||||
|
||||
@@ -41,7 +41,7 @@ public enum JavaVersionConstraint {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaRuntime java) {
|
||||
public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaRuntime java, LibraryAnalyzer analyzer) {
|
||||
GameJavaVersion minimumJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersionNumber);
|
||||
return minimumJavaVersion == null || java.getParsedVersion() >= minimumJavaVersion.majorVersion();
|
||||
}
|
||||
@@ -57,7 +57,7 @@ public enum JavaVersionConstraint {
|
||||
}
|
||||
|
||||
@Override
|
||||
public VersionRange<VersionNumber> getJavaVersionRange(Version version) {
|
||||
public VersionRange<VersionNumber> getJavaVersionRange(Version version, LibraryAnalyzer analyzer) {
|
||||
String javaVersion;
|
||||
if (Objects.requireNonNull(version.getJavaVersion()).majorVersion() >= 9) {
|
||||
javaVersion = "" + version.getJavaVersion().majorVersion();
|
||||
@@ -108,13 +108,26 @@ public enum JavaVersionConstraint {
|
||||
&& super.appliesToVersionImpl(gameVersionNumber, version, java, analyzer);
|
||||
}
|
||||
},
|
||||
CLEANROOM_JAVA_21(true, GameVersionNumber.between("1.12.2", "1.12.999"), VersionNumber.atLeast("21")) {
|
||||
CLEANROOM(true, GameVersionNumber.between("1.12.2", "1.12.999"), VersionRange.all()) {
|
||||
@Override
|
||||
protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version,
|
||||
@Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) {
|
||||
protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version, @Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) {
|
||||
return analyzer != null && analyzer.has(LibraryAnalyzer.LibraryType.CLEANROOM)
|
||||
&& super.appliesToVersionImpl(gameVersionNumber, version, java, analyzer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public VersionRange<VersionNumber> getJavaVersionRange(Version version, LibraryAnalyzer analyzer) {
|
||||
if (analyzer == null || !analyzer.has(LibraryAnalyzer.LibraryType.CLEANROOM))
|
||||
return VersionRange.all();
|
||||
|
||||
String cleanroomVersion = analyzer.getVersion(LibraryAnalyzer.LibraryType.CLEANROOM).orElse("");
|
||||
if (cleanroomVersion.isEmpty())
|
||||
return VersionRange.all();
|
||||
else
|
||||
return VersionNumber.atLeast(
|
||||
String.valueOf(GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion).majorVersion())
|
||||
);
|
||||
}
|
||||
},
|
||||
// LaunchWrapper<=1.12 will crash because LaunchWrapper assumes the system class loader is an instance of URLClassLoader (Java 8)
|
||||
LAUNCH_WRAPPER(true, GameVersionNumber.atMost("1.12.999"), VersionNumber.atMost("1.8.999")) {
|
||||
@@ -143,8 +156,8 @@ public enum JavaVersionConstraint {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaRuntime java) {
|
||||
return java.getArchitecture() != Architecture.X86_64 || super.checkJava(gameVersionNumber, version, java);
|
||||
public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaRuntime java, LibraryAnalyzer analyzer) {
|
||||
return java.getArchitecture() != Architecture.X86_64 || super.checkJava(gameVersionNumber, version, java, analyzer);
|
||||
}
|
||||
},
|
||||
// Minecraft currently does not provide official support for architectures other than x86 and x86-64.
|
||||
@@ -162,7 +175,7 @@ public enum JavaVersionConstraint {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaRuntime java) {
|
||||
public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaRuntime java, LibraryAnalyzer analyzer) {
|
||||
return java.getArchitecture().isX86();
|
||||
}
|
||||
},
|
||||
@@ -194,7 +207,7 @@ public enum JavaVersionConstraint {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaRuntime java) {
|
||||
public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaRuntime java, LibraryAnalyzer analyzer) {
|
||||
int parsedJavaVersion = java.getParsedVersion();
|
||||
if (parsedJavaVersion > 17) {
|
||||
return false;
|
||||
@@ -230,7 +243,7 @@ public enum JavaVersionConstraint {
|
||||
return gameVersionRange;
|
||||
}
|
||||
|
||||
public VersionRange<VersionNumber> getJavaVersionRange(Version version) {
|
||||
public VersionRange<VersionNumber> getJavaVersionRange(Version version, LibraryAnalyzer analyzer) {
|
||||
return javaVersionRange;
|
||||
}
|
||||
|
||||
@@ -251,15 +264,15 @@ public enum JavaVersionConstraint {
|
||||
? String.valueOf(gameJavaVersion.majorVersion())
|
||||
: "1." + gameJavaVersion.majorVersion();
|
||||
|
||||
VersionRange<VersionNumber> range = getJavaVersionRange(version);
|
||||
VersionRange<VersionNumber> range = getJavaVersionRange(version, analyzer);
|
||||
VersionNumber maximum = range.getMaximum();
|
||||
|
||||
return maximum == null || maximum.compareTo(versionNumber) >= 0;
|
||||
}
|
||||
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaRuntime java) {
|
||||
return getJavaVersionRange(version).contains(java.getVersionNumber());
|
||||
public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaRuntime java, LibraryAnalyzer analyzer) {
|
||||
return getJavaVersionRange(version, analyzer).contains(java.getVersionNumber());
|
||||
}
|
||||
|
||||
public static final List<JavaVersionConstraint> ALL = Lang.immutableListOf(values());
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
public enum NativesDirectoryType {
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
public enum Renderer {
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.github.steveice10.opennbt.NBTIO;
|
||||
import com.github.steveice10.opennbt.tag.builtin.*;
|
||||
import javafx.scene.image.Image;
|
||||
import org.glavo.nbt.io.NBTCodec;
|
||||
import org.glavo.nbt.tag.*;
|
||||
import org.jackhuang.hmcl.util.io.*;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -34,9 +34,7 @@ import java.nio.channels.OverlappingFileLockException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -48,6 +46,7 @@ public final class World {
|
||||
private Image icon;
|
||||
|
||||
private CompoundTag levelData;
|
||||
private CompoundTag dataTag;
|
||||
private Path levelDataPath;
|
||||
|
||||
private CompoundTag worldGenSettingsDataBackingTag; // Use for writing back to the file
|
||||
@@ -60,10 +59,64 @@ public final class World {
|
||||
public World(Path file) throws IOException {
|
||||
this.file = file;
|
||||
|
||||
if (Files.isDirectory(file))
|
||||
loadFromDirectory();
|
||||
else if (Files.isRegularFile(file))
|
||||
loadFromZip();
|
||||
if (Files.isDirectory(file)) {
|
||||
fileName = FileUtils.getName(this.file);
|
||||
Path levelDatPath = this.file.resolve("level.dat");
|
||||
if (!Files.exists(levelDatPath)) { // version 20w14infinite
|
||||
levelDatPath = this.file.resolve("special_level.dat");
|
||||
}
|
||||
if (!Files.exists(levelDatPath)) {
|
||||
throw new IOException("Not a valid world directory since level.dat or special_level.dat cannot be found.");
|
||||
}
|
||||
this.levelDataPath = levelDatPath;
|
||||
loadAndCheckWorldData();
|
||||
|
||||
Path iconFile = this.file.resolve("icon.png");
|
||||
if (Files.isRegularFile(iconFile)) {
|
||||
try (InputStream inputStream = Files.newInputStream(iconFile)) {
|
||||
icon = new Image(inputStream, 64, 64, true, false);
|
||||
if (icon.isError())
|
||||
throw icon.getException();
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load world icon", e);
|
||||
}
|
||||
}
|
||||
} else if (Files.isRegularFile(file))
|
||||
try (FileSystem fs = CompressingUtils.readonly(this.file).setAutoDetectEncoding(true).build()) {
|
||||
Path root;
|
||||
if (Files.isRegularFile(fs.getPath("/level.dat"))) {
|
||||
root = fs.getPath("/");
|
||||
fileName = FileUtils.getName(this.file);
|
||||
} else {
|
||||
List<Path> files = Files.list(fs.getPath("/")).toList();
|
||||
if (files.size() != 1 || !Files.isDirectory(files.get(0))) {
|
||||
throw new IOException("Not a valid world zip file");
|
||||
}
|
||||
|
||||
root = files.get(0);
|
||||
fileName = FileUtils.getName(root);
|
||||
}
|
||||
|
||||
Path levelDat = root.resolve("level.dat");
|
||||
if (!Files.exists(levelDat)) { //version 20w14infinite
|
||||
levelDat = root.resolve("special_level.dat");
|
||||
}
|
||||
if (!Files.exists(levelDat)) {
|
||||
throw new IOException("Not a valid world zip file since level.dat or special_level.dat cannot be found.");
|
||||
}
|
||||
loadAndCheckLevelData(levelDat);
|
||||
|
||||
Path iconFile = root.resolve("icon.png");
|
||||
if (Files.isRegularFile(iconFile)) {
|
||||
try (InputStream inputStream = Files.newInputStream(iconFile)) {
|
||||
icon = new Image(inputStream, 64, 64, true, false);
|
||||
if (icon.isError())
|
||||
throw icon.getException();
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load world icon", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
throw new IOException("Path " + file + " cannot be recognized as a Minecraft world");
|
||||
}
|
||||
@@ -77,9 +130,11 @@ public final class World {
|
||||
}
|
||||
|
||||
public String getWorldName() {
|
||||
CompoundTag data = levelData.get("Data");
|
||||
StringTag levelNameTag = data.get("LevelName");
|
||||
return levelNameTag.getValue();
|
||||
if (levelData.get("Data") instanceof CompoundTag data
|
||||
&& data.get("LevelName") instanceof StringTag levelNameTag)
|
||||
return levelNameTag.get();
|
||||
else
|
||||
return "";
|
||||
}
|
||||
|
||||
public void setWorldName(String worldName) throws IOException {
|
||||
@@ -106,9 +161,11 @@ public final class World {
|
||||
}
|
||||
|
||||
public long getLastPlayed() {
|
||||
CompoundTag data = levelData.get("Data");
|
||||
LongTag lastPlayedTag = data.get("LastPlayed");
|
||||
return lastPlayedTag.getValue();
|
||||
if (dataTag.get("LastPlayed") instanceof LongTag lastPlayedTag) {
|
||||
return lastPlayedTag.get();
|
||||
} else {
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable GameVersionNumber getGameVersion() {
|
||||
@@ -122,21 +179,20 @@ public final class World {
|
||||
|
||||
public @Nullable Long getSeed() {
|
||||
// Valid after 1.16(20w20a)
|
||||
if (normalizedWorldGenSettingsData != null && normalizedWorldGenSettingsData.get("seed") instanceof LongTag seedTag) {
|
||||
if (normalizedWorldGenSettingsData != null
|
||||
&& normalizedWorldGenSettingsData.get("seed") instanceof LongTag seedTag) {
|
||||
return seedTag.getValue();
|
||||
}
|
||||
// Valid before 1.16(20w20a)
|
||||
if (levelData.get("Data") instanceof CompoundTag data && data.get("RandomSeed") instanceof LongTag seedTag) {
|
||||
if (dataTag.get("RandomSeed") instanceof LongTag seedTag) {
|
||||
return seedTag.getValue();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean isLargeBiomes() {
|
||||
CompoundTag data = levelData.get("Data");
|
||||
|
||||
// Valid before 1.16(20w20a)
|
||||
if (data.get("generatorName") instanceof StringTag generatorNameTag) {
|
||||
if (dataTag.get("generatorName") instanceof StringTag generatorNameTag) {
|
||||
return "largeBiomes".equals(generatorNameTag.getValue());
|
||||
}
|
||||
// Unified handling of logic after version 1.16
|
||||
@@ -147,11 +203,11 @@ public final class World {
|
||||
// Valid between 1.16(20w20a) and 1.18(21w37a)
|
||||
if (generatorTag.get("biome_source") instanceof CompoundTag biomeSourceTag
|
||||
&& biomeSourceTag.get("large_biomes") instanceof ByteTag largeBiomesTag) {
|
||||
return largeBiomesTag.getValue() == (byte) 1;
|
||||
return largeBiomesTag.get() == (byte) 1;
|
||||
}
|
||||
// Valid after 1.18(21w37a)
|
||||
else if (generatorTag.get("settings") instanceof StringTag settingsTag) {
|
||||
return "minecraft:large_biomes".equals(settingsTag.getValue());
|
||||
return "minecraft:large_biomes".equals(settingsTag.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,79 +234,14 @@ public final class World {
|
||||
return gameVersionNumber != null && gameVersionNumber.isAtLeast("1.20", "23w14a");
|
||||
}
|
||||
|
||||
private void loadFromDirectory() throws IOException {
|
||||
fileName = FileUtils.getName(file);
|
||||
Path levelDat = file.resolve("level.dat");
|
||||
if (!Files.exists(levelDat)) { // version 20w14infinite
|
||||
levelDat = file.resolve("special_level.dat");
|
||||
}
|
||||
if (!Files.exists(levelDat)) {
|
||||
throw new IOException("Not a valid world directory since level.dat or special_level.dat cannot be found.");
|
||||
}
|
||||
this.levelDataPath = levelDat;
|
||||
loadAndCheckWorldData();
|
||||
|
||||
Path iconFile = file.resolve("icon.png");
|
||||
if (Files.isRegularFile(iconFile)) {
|
||||
try (InputStream inputStream = Files.newInputStream(iconFile)) {
|
||||
icon = new Image(inputStream, 64, 64, true, false);
|
||||
if (icon.isError())
|
||||
throw icon.getException();
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load world icon", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadFromZipImpl(Path root) throws IOException {
|
||||
Path levelDat = root.resolve("level.dat");
|
||||
if (!Files.exists(levelDat)) { //version 20w14infinite
|
||||
levelDat = root.resolve("special_level.dat");
|
||||
}
|
||||
if (!Files.exists(levelDat)) {
|
||||
throw new IOException("Not a valid world zip file since level.dat or special_level.dat cannot be found.");
|
||||
}
|
||||
loadAndCheckLevelData(levelDat);
|
||||
|
||||
Path iconFile = root.resolve("icon.png");
|
||||
if (Files.isRegularFile(iconFile)) {
|
||||
try (InputStream inputStream = Files.newInputStream(iconFile)) {
|
||||
icon = new Image(inputStream, 64, 64, true, false);
|
||||
if (icon.isError())
|
||||
throw icon.getException();
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load world icon", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadFromZip() throws IOException {
|
||||
try (FileSystem fs = CompressingUtils.readonly(file).setAutoDetectEncoding(true).build()) {
|
||||
Path levelDatPath = fs.getPath("/level.dat");
|
||||
if (Files.isRegularFile(levelDatPath)) {
|
||||
fileName = FileUtils.getName(file);
|
||||
loadFromZipImpl(fs.getPath("/"));
|
||||
return;
|
||||
}
|
||||
|
||||
try (Stream<Path> stream = Files.list(fs.getPath("/"))) {
|
||||
Path root = stream.filter(Files::isDirectory).findAny()
|
||||
.orElseThrow(() -> new IOException("Not a valid world zip file"));
|
||||
fileName = FileUtils.getName(root);
|
||||
loadFromZipImpl(root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadAndCheckWorldData() throws IOException {
|
||||
loadAndCheckLevelData(levelDataPath);
|
||||
loadOtherData();
|
||||
}
|
||||
|
||||
private void loadAndCheckLevelData(Path levelDat) throws IOException {
|
||||
this.levelData = readTag(levelDat);
|
||||
CompoundTag data = levelData.get("Data");
|
||||
if (data == null)
|
||||
this.levelData = NBTCodec.of().readTag(levelDat, TagType.COMPOUND);
|
||||
if (!(levelData.get("Data") instanceof CompoundTag data))
|
||||
throw new IOException("level.dat missing Data");
|
||||
|
||||
if (!(data.get("LevelName") instanceof StringTag))
|
||||
@@ -258,6 +249,7 @@ public final class World {
|
||||
|
||||
if (!(data.get("LastPlayed") instanceof LongTag))
|
||||
throw new IOException("level.dat missing LastPlayed");
|
||||
this.dataTag = data;
|
||||
}
|
||||
|
||||
private void loadOtherData() throws IOException {
|
||||
@@ -267,7 +259,7 @@ public final class World {
|
||||
if (data.get("WorldGenSettings") instanceof CompoundTag worldGenSettingsTag) {
|
||||
setWorldGenSettingsData(null, worldGenSettingsTag, worldGenSettingsTag);
|
||||
} else if (Files.isRegularFile(worldGenSettingsDatPath)) {
|
||||
CompoundTag raw = readTag(worldGenSettingsDatPath);
|
||||
CompoundTag raw = NBTCodec.of().readTag(worldGenSettingsDatPath, TagType.COMPOUND);
|
||||
if (raw.get("data") instanceof CompoundTag compoundTag) {
|
||||
setWorldGenSettingsData(worldGenSettingsDatPath, raw, compoundTag);
|
||||
} else {
|
||||
@@ -279,14 +271,11 @@ public final class World {
|
||||
|
||||
if (data.get("Player") instanceof CompoundTag playerTag) {
|
||||
setPlayerData(null, playerTag);
|
||||
} else if (data.get("singleplayer_uuid") instanceof IntArrayTag uuidTag && uuidTag.getValue().length == 4) {
|
||||
int[] uuidValue = uuidTag.getValue();
|
||||
long mostSigBits = ((long) uuidValue[0] << 32) | (uuidValue[1] & 0xFFFFFFFFL);
|
||||
long leastSigBits = ((long) uuidValue[2] << 32) | (uuidValue[3] & 0xFFFFFFFFL);
|
||||
String playerUUID = new UUID(mostSigBits, leastSigBits).toString();
|
||||
} else if (data.get("singleplayer_uuid") instanceof IntArrayTag uuidTag && uuidTag.isUUID()) {
|
||||
String playerUUID = uuidTag.getUUID().toString();
|
||||
Path playerDatPath = file.resolve("players/data/" + playerUUID + ".dat");
|
||||
if (Files.exists(playerDatPath)) {
|
||||
setPlayerData(playerDatPath, readTag(playerDatPath));
|
||||
setPlayerData(playerDatPath, NBTCodec.of().readTag(playerDatPath, TagType.COMPOUND));
|
||||
} else {
|
||||
setPlayerData(null, null);
|
||||
}
|
||||
@@ -317,8 +306,7 @@ public final class World {
|
||||
throw new IOException("Not a valid world directory");
|
||||
|
||||
// Change the name recorded in level.dat
|
||||
CompoundTag data = levelData.get("Data");
|
||||
data.put(new StringTag("LevelName", newName));
|
||||
dataTag.setString("LevelName", newName);
|
||||
writeLevelData();
|
||||
|
||||
// then change the folder's name
|
||||
@@ -433,19 +421,11 @@ public final class World {
|
||||
writeTag(levelData, levelDataPath);
|
||||
}
|
||||
|
||||
private CompoundTag readTag(Path path) throws IOException {
|
||||
try (InputStream is = new GZIPInputStream(Files.newInputStream(path))) {
|
||||
Tag tag = NBTIO.readTag(is);
|
||||
if (tag instanceof CompoundTag compoundTag) return compoundTag;
|
||||
throw new IOException("NBT file malformed: " + path);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeTag(CompoundTag nbt, Path path) throws IOException {
|
||||
if (!Files.isDirectory(file)) throw new IOException("Not a valid world directory");
|
||||
FileUtils.saveSafely(path, os -> {
|
||||
try (OutputStream gos = new GZIPOutputStream(os)) {
|
||||
NBTIO.writeTag(gos, nbt);
|
||||
NBTCodec.of().writeTag(gos, nbt);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.launch;
|
||||
|
||||
public class CommandTooLongException extends RuntimeException {
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.launch;
|
||||
|
||||
public final class ExecutionPolicyLimitException extends RuntimeException {
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.mod;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -177,10 +178,10 @@ public final class LocalModFile implements Comparable<LocalModFile> {
|
||||
file = modManager.disableMod(file);
|
||||
}
|
||||
|
||||
public ModUpdate checkUpdates(String gameVersion, RemoteModRepository repository) throws IOException {
|
||||
public ModUpdate checkUpdates(DownloadProvider downloadProvider, String gameVersion, RemoteModRepository repository) throws IOException {
|
||||
Optional<RemoteMod.Version> currentVersion = repository.getRemoteVersionByLocalFile(this, file);
|
||||
if (!currentVersion.isPresent()) return null;
|
||||
List<RemoteMod.Version> remoteVersions = repository.getRemoteVersionsById(currentVersion.get().getModid())
|
||||
List<RemoteMod.Version> remoteVersions = repository.getRemoteVersionsById(downloadProvider, currentVersion.get().getModid())
|
||||
.filter(version -> version.getGameVersions().contains(gameVersion))
|
||||
.filter(version -> version.getLoaders().contains(getModLoaderType()))
|
||||
.filter(version -> version.getDatePublished().compareTo(currentVersion.get().getDatePublished()) > 0)
|
||||
|
||||
@@ -23,12 +23,17 @@ import org.jackhuang.hmcl.task.Task;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public abstract class Modpack {
|
||||
|
||||
public static final Set<String> SUPPORTED_ICON_EXTS = Set.of("png", "jpg", "jpeg", "bmp", "gif", "webp", "apng");
|
||||
public static final Set<String> SUPPORTED_ICON_NAMES = Set.of("icon.png", "icon.jpg", "icon.jpeg", "icon.bmp", "icon.gif", "icon.webp", "icon.apng");
|
||||
|
||||
private String name;
|
||||
private String author;
|
||||
private String version;
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.mod;
|
||||
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.mod;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
@@ -32,12 +33,12 @@ public final class RemoteMod {
|
||||
|
||||
public static final RemoteMod BROKEN = new RemoteMod("", "", "RemoteMod.BROKEN", "", Collections.emptyList(), "", "", new RemoteMod.IMod() {
|
||||
@Override
|
||||
public List<RemoteMod> loadDependencies(RemoteModRepository modRepository) throws IOException {
|
||||
public List<RemoteMod> loadDependencies(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException {
|
||||
throw new IOException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<RemoteMod.Version> loadVersions(RemoteModRepository modRepository) throws IOException {
|
||||
public Stream<RemoteMod.Version> loadVersions(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException {
|
||||
throw new IOException();
|
||||
}
|
||||
});
|
||||
@@ -154,12 +155,12 @@ public final class RemoteMod {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public RemoteMod load() throws IOException {
|
||||
public RemoteMod load(DownloadProvider downloadProvider) throws IOException {
|
||||
if (this.remoteMod == null) {
|
||||
if (this.type == DependencyType.BROKEN) {
|
||||
this.remoteMod = RemoteMod.BROKEN;
|
||||
} else {
|
||||
this.remoteMod = this.remoteModRepository.resolveDependency(this.id);
|
||||
this.remoteMod = this.remoteModRepository.resolveDependency(downloadProvider, this.id);
|
||||
}
|
||||
}
|
||||
return this.remoteMod;
|
||||
@@ -202,9 +203,9 @@ public final class RemoteMod {
|
||||
}
|
||||
|
||||
public interface IMod {
|
||||
List<RemoteMod> loadDependencies(RemoteModRepository modRepository) throws IOException;
|
||||
List<RemoteMod> loadDependencies(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException;
|
||||
|
||||
Stream<Version> loadVersions(RemoteModRepository modRepository) throws IOException;
|
||||
Stream<Version> loadVersions(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException;
|
||||
}
|
||||
|
||||
public interface IVersion {
|
||||
|
||||
@@ -90,15 +90,15 @@ public interface RemoteModRepository {
|
||||
|
||||
Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException;
|
||||
|
||||
RemoteMod getModById(String id) throws IOException;
|
||||
RemoteMod getModById(DownloadProvider downloadProvider, String id) throws IOException;
|
||||
|
||||
default RemoteMod resolveDependency(String id) throws IOException {
|
||||
return getModById(id);
|
||||
default RemoteMod resolveDependency(DownloadProvider downloadProvider, String id) throws IOException {
|
||||
return getModById(downloadProvider, id);
|
||||
}
|
||||
|
||||
RemoteMod.File getModFile(String modId, String fileId) throws IOException;
|
||||
|
||||
Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException;
|
||||
Stream<RemoteMod.Version> getRemoteVersionsById(DownloadProvider downloadProvider, String id) throws IOException;
|
||||
|
||||
Stream<Category> getCategories() throws IOException;
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.mod.curse;
|
||||
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
@@ -191,7 +192,7 @@ public class CurseAddon implements RemoteMod.IMod {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RemoteMod> loadDependencies(RemoteModRepository modRepository) throws IOException {
|
||||
public List<RemoteMod> loadDependencies(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException {
|
||||
Set<Integer> dependencies = latestFiles.stream()
|
||||
.flatMap(latestFile -> latestFile.getDependencies().stream())
|
||||
.filter(dep -> dep.getRelationType() == 3)
|
||||
@@ -199,14 +200,14 @@ public class CurseAddon implements RemoteMod.IMod {
|
||||
.collect(Collectors.toSet());
|
||||
List<RemoteMod> mods = new ArrayList<>();
|
||||
for (int dependencyId : dependencies) {
|
||||
mods.add(modRepository.getModById(Integer.toString(dependencyId)));
|
||||
mods.add(modRepository.getModById(downloadProvider, Integer.toString(dependencyId)));
|
||||
}
|
||||
return mods;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<RemoteMod.Version> loadVersions(RemoteModRepository modRepository) throws IOException {
|
||||
return modRepository.getRemoteVersionsById(Integer.toString(id));
|
||||
public Stream<RemoteMod.Version> loadVersions(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException {
|
||||
return modRepository.getRemoteVersionsById(downloadProvider, Integer.toString(id));
|
||||
}
|
||||
|
||||
public RemoteMod toMod() {
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.mod.curse;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.game.DefaultGameRepository;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.mod.ModpackCompletionException;
|
||||
@@ -147,7 +148,7 @@ public final class CurseCompletionTask extends Task<Void> {
|
||||
.filter(f -> f.fileName() != null)
|
||||
.flatMap(f -> {
|
||||
try {
|
||||
Path path = guessFilePath(f, resourcePacksRoot, shaderPacksRoot);
|
||||
Path path = guessFilePath(f, dependency.getDownloadProvider(), resourcePacksRoot, shaderPacksRoot);
|
||||
if (path == null) {
|
||||
return Stream.empty();
|
||||
}
|
||||
@@ -175,13 +176,14 @@ public final class CurseCompletionTask extends Task<Void> {
|
||||
* Guess where to store the file.
|
||||
*
|
||||
* @param file The file.
|
||||
* @param downloadProvider
|
||||
* @param resourcePacksRoot ./resourcepacks.
|
||||
* @param shaderPacksRoot ./shaderpacks.
|
||||
* @return ./resourcepacks/$filename or ./shaderpacks/$filename or ./mods/$filename if the file doesn't exist. null if the file existed.
|
||||
* @throws IOException If IOException was encountered during getting data from CurseForge.
|
||||
*/
|
||||
private Path guessFilePath(CurseManifestFile file, Path resourcePacksRoot, Path shaderPacksRoot) throws IOException {
|
||||
RemoteMod mod = CurseForgeRemoteModRepository.MODS.getModById(Integer.toString(file.projectID()));
|
||||
private Path guessFilePath(CurseManifestFile file, DownloadProvider downloadProvider, Path resourcePacksRoot, Path shaderPacksRoot) throws IOException {
|
||||
RemoteMod mod = CurseForgeRemoteModRepository.MODS.getModById(downloadProvider, Integer.toString(file.projectID()));
|
||||
int classID = ((CurseAddon) mod.getData()).getClassId();
|
||||
String fileName = file.fileName();
|
||||
return switch (classID) {
|
||||
|
||||
+4
-2
@@ -227,7 +227,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
}
|
||||
|
||||
@Override
|
||||
public RemoteMod getModById(String id) throws IOException {
|
||||
public RemoteMod getModById(DownloadProvider downloadProvider, String id) throws IOException {
|
||||
SEMAPHORE.acquireUninterruptibly();
|
||||
try {
|
||||
Response<CurseAddon> response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id))
|
||||
@@ -251,7 +251,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
|
||||
public Stream<RemoteMod.Version> getRemoteVersionsById(DownloadProvider downloadProvider, String id) throws IOException {
|
||||
SEMAPHORE.acquireUninterruptibly();
|
||||
try {
|
||||
Response<List<CurseAddon.LatestFile>> response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id + "/files",
|
||||
@@ -302,6 +302,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
public static final int SECTION_RESOURCE_PACK = 12;
|
||||
public static final int SECTION_WORLD = 17;
|
||||
public static final int SECTION_MODPACK = 4471;
|
||||
public static final int SECTION_SHADER = 6552;
|
||||
public static final int SECTION_CUSTOMIZATION = 4546;
|
||||
public static final int SECTION_ADDONS = 4559; // For Pocket Edition
|
||||
public static final int SECTION_UNKNOWN1 = 4944;
|
||||
@@ -313,6 +314,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
public static final CurseForgeRemoteModRepository RESOURCE_PACKS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.RESOURCE_PACK, SECTION_RESOURCE_PACK);
|
||||
public static final CurseForgeRemoteModRepository WORLDS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.WORLD, SECTION_WORLD);
|
||||
public static final CurseForgeRemoteModRepository CUSTOMIZATIONS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.CUSTOMIZATION, SECTION_CUSTOMIZATION);
|
||||
public static final CurseForgeRemoteModRepository SHADERS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.SHADER_PACK, SECTION_SHADER);
|
||||
|
||||
public static class Pagination {
|
||||
private final int index;
|
||||
|
||||
@@ -22,17 +22,20 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.GameBuilder;
|
||||
import org.jackhuang.hmcl.game.DefaultGameRepository;
|
||||
import org.jackhuang.hmcl.mod.*;
|
||||
import org.jackhuang.hmcl.task.CacheFileTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
* Install a downloaded CurseForge modpack.
|
||||
@@ -47,8 +50,11 @@ public final class CurseInstallTask extends Task<Void> {
|
||||
private final Modpack modpack;
|
||||
private final CurseManifest manifest;
|
||||
private final String name;
|
||||
private final String iconUrl;
|
||||
private final Path run;
|
||||
private final ModpackConfiguration<CurseManifest> config;
|
||||
private String iconExt;
|
||||
private Task<Path> downloadIconTask;
|
||||
private final List<Task<?>> dependents = new ArrayList<>(4);
|
||||
private final List<Task<?>> dependencies = new ArrayList<>(1);
|
||||
|
||||
@@ -60,12 +66,13 @@ public final class CurseInstallTask extends Task<Void> {
|
||||
* @param manifest The manifest content of given CurseForge modpack.
|
||||
* @param name the new version name
|
||||
*/
|
||||
public CurseInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, Modpack modpack, CurseManifest manifest, String name) {
|
||||
public CurseInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, Modpack modpack, CurseManifest manifest, String name, String iconUrl) {
|
||||
this.dependencyManager = dependencyManager;
|
||||
this.zipFile = zipFile;
|
||||
this.modpack = modpack;
|
||||
this.manifest = manifest;
|
||||
this.name = name;
|
||||
this.iconUrl = iconUrl;
|
||||
this.repository = dependencyManager.getGameRepository();
|
||||
this.run = repository.getRunDirectory(name);
|
||||
|
||||
@@ -108,6 +115,14 @@ public final class CurseInstallTask extends Task<Void> {
|
||||
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList(manifest.overrides()), any -> true, config).withStage("hmcl.modpack"));
|
||||
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList(manifest.overrides()), manifest, CurseModpackProvider.INSTANCE, manifest.name(), manifest.version(), repository.getModpackConfiguration(name)).withStage("hmcl.modpack"));
|
||||
|
||||
URI iconUri = NetworkUtils.toURIOrNull(iconUrl);
|
||||
if (iconUri != null) {
|
||||
String ext = FileUtils.getExtension(StringUtils.substringAfter(iconUri.getPath(), '/')).toLowerCase(Locale.ROOT);
|
||||
if (Modpack.SUPPORTED_ICON_EXTS.contains(ext)) {
|
||||
iconExt = ext;
|
||||
dependents.add(downloadIconTask = new CacheFileTask(dependencyManager.getDownloadProvider().injectURLWithCandidates(iconUrl)));
|
||||
}
|
||||
}
|
||||
dependencies.add(new CurseCompletionTask(dependencyManager, name, manifest));
|
||||
}
|
||||
|
||||
@@ -137,5 +152,13 @@ public final class CurseInstallTask extends Task<Void> {
|
||||
Path root = repository.getVersionRoot(name);
|
||||
Files.createDirectories(root);
|
||||
JsonUtils.writeToJsonFile(root.resolve("manifest.json"), manifest);
|
||||
|
||||
if (iconExt != null && Modpack.SUPPORTED_ICON_NAMES.stream().map(root::resolve).allMatch(Files::notExists)) {
|
||||
try {
|
||||
Files.copy(downloadIconTask.getResult(), root.resolve("icon." + iconExt));
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to copy modpack icon", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ public final class CurseModpackProvider implements ModpackProvider {
|
||||
if (!(modpack.getManifest() instanceof CurseManifest curseManifest))
|
||||
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
|
||||
|
||||
return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new CurseInstallTask(dependencyManager, zipFile, modpack, curseManifest, name));
|
||||
return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new CurseInstallTask(dependencyManager, zipFile, modpack, curseManifest, name, null));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -69,7 +69,7 @@ public final class CurseModpackProvider implements ModpackProvider {
|
||||
return new Modpack(manifest.name(), manifest.author(), manifest.version(), manifest.minecraft().gameVersion(), description, encoding, manifest) {
|
||||
@Override
|
||||
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name, String iconUrl) {
|
||||
return new CurseInstallTask(dependencyManager, zipFile, this, manifest, name);
|
||||
return new CurseInstallTask(dependencyManager, zipFile, this, manifest, name, iconUrl);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.mod.modinfo;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.mod.modinfo;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
@@ -25,7 +25,6 @@ import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
@@ -36,7 +35,6 @@ import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@@ -123,7 +121,7 @@ public class ModrinthCompletionTask extends Task<Void> {
|
||||
continue;
|
||||
|
||||
var task = new FileDownloadTask(
|
||||
file.getDownloads().stream().map(NetworkUtils::toURI).collect(Collectors.toList()),
|
||||
dependency.getDownloadProvider().injectURLsWithCandidates(file.getDownloads()),
|
||||
filePath);
|
||||
task.setCacheRepository(dependency.getCacheRepository());
|
||||
task.setCaching(true);
|
||||
|
||||
@@ -38,7 +38,6 @@ import java.util.*;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class ModrinthInstallTask extends Task<Void> {
|
||||
private static final Set<String> SUPPORTED_ICON_EXTS = Set.of("png", "jpg", "jpeg", "bmp", "gif", "webp", "apng");
|
||||
|
||||
private final DefaultDependencyManager dependencyManager;
|
||||
private final DefaultGameRepository repository;
|
||||
@@ -121,9 +120,10 @@ public class ModrinthInstallTask extends Task<Void> {
|
||||
URI iconUri = NetworkUtils.toURIOrNull(iconUrl);
|
||||
if (iconUri != null) {
|
||||
String ext = FileUtils.getExtension(StringUtils.substringAfter(iconUri.getPath(), '/')).toLowerCase(Locale.ROOT);
|
||||
if (SUPPORTED_ICON_EXTS.contains(ext)) {
|
||||
if (Modpack.SUPPORTED_ICON_EXTS.contains(ext)) {
|
||||
iconExt = ext;
|
||||
dependents.add(downloadIconTask = new CacheFileTask(iconUrl));
|
||||
|
||||
dependents.add(downloadIconTask = new CacheFileTask(dependencyManager.getDownloadProvider().injectURLWithCandidates(iconUrl)));
|
||||
}
|
||||
}
|
||||
dependencies.add(new ModrinthCompletionTask(dependencyManager, name, manifest));
|
||||
@@ -156,7 +156,7 @@ public class ModrinthInstallTask extends Task<Void> {
|
||||
Files.createDirectories(root);
|
||||
JsonUtils.writeToJsonFile(root.resolve("modrinth.index.json"), manifest);
|
||||
|
||||
if (iconExt != null) {
|
||||
if (iconExt != null && Modpack.SUPPORTED_ICON_NAMES.stream().map(root::resolve).allMatch(Files::notExists)) {
|
||||
try {
|
||||
Files.copy(downloadIconTask.getResult(), root.resolve("icon." + iconExt));
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.mod.modrinth;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
+62
-22
@@ -115,7 +115,6 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
||||
pair("index", convertSortType(sort))
|
||||
);
|
||||
|
||||
|
||||
List<URI> candidates = downloadProvider.injectURLWithCandidates(NetworkUtils.withQuery(PREFIX + "/v2/search", query));
|
||||
IOException exception = null;
|
||||
for (URI candidate : candidates) {
|
||||
@@ -126,13 +125,15 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
||||
return new SearchResult(response.getHits().stream().map(ProjectSearchResult::toMod), (int) Math.ceil((double) response.totalHits / pageSize));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to search addons: " + candidate, e);
|
||||
|
||||
IOException wrapper = new IOException("Failed to search addons: " + candidate, e);
|
||||
if (candidates.size() == 1) {
|
||||
exception = e;
|
||||
exception = wrapper;
|
||||
} else {
|
||||
if (exception == null) {
|
||||
exception = new IOException("Failed to search addons");
|
||||
}
|
||||
exception.addSuppressed(e);
|
||||
exception.addSuppressed(wrapper);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,21 +168,40 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public RemoteMod getModById(String id) throws IOException {
|
||||
public RemoteMod getModById(DownloadProvider downloadProvider, String id) throws IOException {
|
||||
SEMAPHORE.acquireUninterruptibly();
|
||||
try {
|
||||
id = StringUtils.removePrefix(id, "local-");
|
||||
Project project = HttpRequest.GET(PREFIX + "/v2/project/" + id).getJson(Project.class);
|
||||
return project.toMod();
|
||||
List<URI> candidates = downloadProvider.injectURLWithCandidates(PREFIX + "/v2/project/" + id);
|
||||
IOException exception = null;
|
||||
|
||||
for (URI candidate : candidates) {
|
||||
try {
|
||||
Project project = HttpRequest.GET(candidate.toString()).getJson(Project.class);
|
||||
return project.toMod();
|
||||
} catch (IOException e) {
|
||||
IOException wrapper = new IOException("Failed to get mod: " + candidate, e);
|
||||
if (candidates.size() == 1) {
|
||||
exception = wrapper;
|
||||
} else {
|
||||
if (exception == null) {
|
||||
exception = new IOException("Failed to get mod");
|
||||
}
|
||||
exception.addSuppressed(wrapper);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw exception != null ? exception : new IOException("No candidates found");
|
||||
} finally {
|
||||
SEMAPHORE.release();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RemoteMod resolveDependency(String id) throws IOException {
|
||||
public RemoteMod resolveDependency(DownloadProvider downloadProvider, String id) throws IOException {
|
||||
try {
|
||||
return getModById(id);
|
||||
return getModById(downloadProvider, id);
|
||||
} catch (ResponseCodeException e) {
|
||||
if (e.getResponseCode() == 502 || e.getResponseCode() == 404) {
|
||||
return RemoteMod.BROKEN;
|
||||
@@ -198,13 +218,33 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<RemoteMod.Version> getRemoteVersionsById(String id) throws IOException {
|
||||
public Stream<RemoteMod.Version> getRemoteVersionsById(DownloadProvider downloadProvider, String id) throws IOException {
|
||||
SEMAPHORE.acquireUninterruptibly();
|
||||
try {
|
||||
id = StringUtils.removePrefix(id, "local-");
|
||||
List<ProjectVersion> versions = HttpRequest.GET(PREFIX + "/v2/project/" + id + "/version?include_changelog=false")
|
||||
.getJson(listTypeOf(ProjectVersion.class));
|
||||
return versions.stream().map(ProjectVersion::toVersion).flatMap(Lang::toStream);
|
||||
|
||||
List<URI> candidates = downloadProvider.injectURLWithCandidates(PREFIX + "/v2/project/" + id + "/version?include_changelog=false");
|
||||
IOException exception = null;
|
||||
|
||||
for (URI candidate : candidates) {
|
||||
try {
|
||||
List<ProjectVersion> versions = HttpRequest.GET(candidate.toString())
|
||||
.getJson(listTypeOf(ProjectVersion.class));
|
||||
return versions.stream().map(ProjectVersion::toVersion).flatMap(Lang::toStream);
|
||||
} catch (IOException e) {
|
||||
IOException wrapper = new IOException("Failed to get remote versions: " + candidate, e);
|
||||
if (candidates.size() == 1) {
|
||||
exception = wrapper;
|
||||
} else {
|
||||
if (exception == null) {
|
||||
exception = new IOException("Failed to get remote versions");
|
||||
}
|
||||
exception.addSuppressed(wrapper);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw exception != null ? exception : new IOException("No candidates found");
|
||||
} finally {
|
||||
SEMAPHORE.release();
|
||||
}
|
||||
@@ -362,20 +402,20 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RemoteMod> loadDependencies(RemoteModRepository modRepository) throws IOException {
|
||||
Set<RemoteMod.Dependency> dependencies = modRepository.getRemoteVersionsById(getId())
|
||||
public List<RemoteMod> loadDependencies(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException {
|
||||
Set<RemoteMod.Dependency> dependencies = modRepository.getRemoteVersionsById(downloadProvider, getId())
|
||||
.flatMap(version -> version.getDependencies().stream())
|
||||
.collect(Collectors.toSet());
|
||||
List<RemoteMod> mods = new ArrayList<>();
|
||||
for (RemoteMod.Dependency dependency : dependencies) {
|
||||
mods.add(dependency.load());
|
||||
mods.add(dependency.load(downloadProvider));
|
||||
}
|
||||
return mods;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<RemoteMod.Version> loadVersions(RemoteModRepository modRepository) throws IOException {
|
||||
return modRepository.getRemoteVersionsById(getId());
|
||||
public Stream<RemoteMod.Version> loadVersions(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException {
|
||||
return modRepository.getRemoteVersionsById(downloadProvider, getId());
|
||||
}
|
||||
|
||||
public RemoteMod toMod() {
|
||||
@@ -749,20 +789,20 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RemoteMod> loadDependencies(RemoteModRepository modRepository) throws IOException {
|
||||
Set<RemoteMod.Dependency> dependencies = modRepository.getRemoteVersionsById(getProjectId())
|
||||
public List<RemoteMod> loadDependencies(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException {
|
||||
Set<RemoteMod.Dependency> dependencies = modRepository.getRemoteVersionsById(downloadProvider, getProjectId())
|
||||
.flatMap(version -> version.getDependencies().stream())
|
||||
.collect(Collectors.toSet());
|
||||
List<RemoteMod> mods = new ArrayList<>();
|
||||
for (RemoteMod.Dependency dependency : dependencies) {
|
||||
mods.add(dependency.load());
|
||||
mods.add(dependency.load(downloadProvider));
|
||||
}
|
||||
return mods;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<RemoteMod.Version> loadVersions(RemoteModRepository modRepository) throws IOException {
|
||||
return modRepository.getRemoteVersionsById(getProjectId());
|
||||
public Stream<RemoteMod.Version> loadVersions(RemoteModRepository modRepository, DownloadProvider downloadProvider) throws IOException {
|
||||
return modRepository.getRemoteVersionsById(downloadProvider, getProjectId());
|
||||
}
|
||||
|
||||
public RemoteMod toMod() {
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.mod.multimc;
|
||||
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.resourcepack;
|
||||
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.resourcepack;
|
||||
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.resourcepack;
|
||||
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.schematic;
|
||||
|
||||
import com.github.steveice10.opennbt.NBTIO;
|
||||
import com.github.steveice10.opennbt.tag.builtin.*;
|
||||
import javafx.geometry.Point3D;
|
||||
import org.glavo.nbt.io.NBTCodec;
|
||||
import org.glavo.nbt.tag.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -30,32 +30,19 @@ import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
* @see <a href="https://litemapy.readthedocs.io/en/v0.9.0b0/litematics.html">The Litematic file format</a>
|
||||
*/
|
||||
/// @author Glavo
|
||||
/// @see <a href="https://litemapy.readthedocs.io/en/v0.9.0b0/litematics.html">The Litematic file format</a>
|
||||
public final class LitematicFile {
|
||||
|
||||
private static int tryGetInt(Tag tag) {
|
||||
return tag instanceof IntTag ? ((IntTag) tag).getValue() : 0;
|
||||
}
|
||||
|
||||
private static @Nullable Instant tryGetLongTimestamp(Tag tag) {
|
||||
if (tag instanceof LongTag) {
|
||||
return Instant.ofEpochMilli(((LongTag) tag).getValue());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static @Nullable String tryGetString(Tag tag) {
|
||||
return tag instanceof StringTag ? ((StringTag) tag).getValue() : null;
|
||||
return tag instanceof StringTag stringTag ? stringTag.get() : null;
|
||||
}
|
||||
|
||||
public static LitematicFile load(Path file) throws IOException {
|
||||
|
||||
CompoundTag root;
|
||||
try (InputStream in = new GZIPInputStream(Files.newInputStream(file))) {
|
||||
root = (CompoundTag) NBTIO.readTag(in);
|
||||
root = NBTCodec.of().readTag(in, TagType.COMPOUND);
|
||||
}
|
||||
|
||||
Tag versionTag = root.get("Version");
|
||||
@@ -71,14 +58,13 @@ public final class LitematicFile {
|
||||
throw new IOException("Metadata tag is not a compound tag");
|
||||
|
||||
int regions = 0;
|
||||
Tag regionsTag = root.get("Regions");
|
||||
if (regionsTag instanceof CompoundTag)
|
||||
regions = ((CompoundTag) regionsTag).size();
|
||||
if (root.get("Regions") instanceof CompoundTag regionsTag)
|
||||
regions = regionsTag.size();
|
||||
|
||||
return new LitematicFile(file, (CompoundTag) metadataTag,
|
||||
((IntTag) versionTag).getValue(),
|
||||
tryGetInt(root.get("SubVersion")),
|
||||
tryGetInt(root.get("MinecraftDataVersion")),
|
||||
root.getIntOrZero("SubVersion"),
|
||||
root.getIntOrZero("MinecraftDataVersion"),
|
||||
regions
|
||||
);
|
||||
}
|
||||
@@ -108,26 +94,23 @@ public final class LitematicFile {
|
||||
this.regionCount = regionCount;
|
||||
|
||||
Tag previewImageData = metadata.get("PreviewImageData");
|
||||
this.previewImageData = previewImageData instanceof IntArrayTag
|
||||
? ((IntArrayTag) previewImageData).getValue()
|
||||
this.previewImageData = previewImageData instanceof IntArrayTag intArrayTag
|
||||
? intArrayTag.getArray()
|
||||
: null;
|
||||
|
||||
this.name = tryGetString(metadata.get("Name"));
|
||||
this.author = tryGetString(metadata.get("Author"));
|
||||
this.description = tryGetString(metadata.get("Description"));
|
||||
this.timeCreated = tryGetLongTimestamp(metadata.get("TimeCreated"));
|
||||
this.timeModified = tryGetLongTimestamp(metadata.get("TimeModified"));
|
||||
this.totalBlocks = tryGetInt(metadata.get("TotalBlocks"));
|
||||
this.totalVolume = tryGetInt(metadata.get("TotalVolume"));
|
||||
|
||||
this.timeCreated = metadata.get("TimeCreated") instanceof LongTag time ? Instant.ofEpochMilli(time.getValue()) : null;
|
||||
this.timeModified = metadata.get("TimeModified") instanceof LongTag time ? Instant.ofEpochMilli(time.getValue()) : null;
|
||||
this.totalBlocks = metadata.getIntOrZero("TotalBlocks");
|
||||
this.totalVolume = metadata.getIntOrZero("TotalVolume");
|
||||
|
||||
Point3D enclosingSize = null;
|
||||
Tag enclosingSizeTag = metadata.get("EnclosingSize");
|
||||
if (enclosingSizeTag instanceof CompoundTag) {
|
||||
CompoundTag list = (CompoundTag) enclosingSizeTag;
|
||||
int x = tryGetInt(list.get("x"));
|
||||
int y = tryGetInt(list.get("y"));
|
||||
int z = tryGetInt(list.get("z"));
|
||||
if (metadata.get("EnclosingSize") instanceof CompoundTag list) {
|
||||
int x = list.getIntOrZero("x");
|
||||
int y = list.getIntOrZero("y");
|
||||
int z = list.getIntOrZero("z");
|
||||
|
||||
if (x >= 0 && y >= 0 && z >= 0)
|
||||
enclosingSize = new Point3D(x, y, z);
|
||||
|
||||
Loaded 100 of 133 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user