Compare commits
43
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4ac42d789 | ||
|
|
32aafb11a3 | ||
|
|
36dab70b7a | ||
|
|
ebf253b1a7 | ||
|
|
539dcca5ca | ||
|
|
659b6c2a70 | ||
|
|
3dcc7ab311 | ||
|
|
e0ccb58272 | ||
|
|
d4ec8ebec8 | ||
|
|
f3b21e5099 | ||
|
|
202fd38180 | ||
|
|
f5768f2f95 | ||
|
|
2d377d06de | ||
|
|
42d1581ba0 | ||
|
|
995a23c2bc | ||
|
|
8969f55a1f | ||
|
|
312a285792 | ||
|
|
d85b052195 | ||
|
|
cd0d3d1b97 | ||
|
|
6ccc7df998 | ||
|
|
73e5909a69 | ||
|
|
ade487277d | ||
|
|
e755d17977 | ||
|
|
292288de9e | ||
|
|
57f6dec462 | ||
|
|
8e1a549c9d | ||
|
|
1d18eae377 | ||
|
|
6ef04d2fa5 | ||
|
|
1778e9d916 | ||
|
|
dbb63e9faa | ||
|
|
6b1c3182eb | ||
|
|
82460ebbcb | ||
|
|
3e8a9bf1c4 | ||
|
|
a2db925e86 | ||
|
|
fe70cc3daf | ||
|
|
5991112cd8 | ||
|
|
084d91c62a | ||
|
|
4963f7a5f0 | ||
|
|
f42d5219c2 | ||
|
|
101a9b5d5e | ||
|
|
7cd79a4e2a | ||
|
|
30c3acfb23 | ||
|
|
31f04db555 |
No files matched your search
+7
-1
@@ -64,4 +64,10 @@ language-subtag-registry
|
||||
# test
|
||||
/hmcl.json
|
||||
/.hmcl.json
|
||||
/.hmcl/
|
||||
/.hmcl/
|
||||
|
||||
# AI
|
||||
/.gemini/
|
||||
!/.gemini/config.yaml
|
||||
.codex
|
||||
/.gradle-*/
|
||||
@@ -0,0 +1,27 @@
|
||||
# Java Code Style Requirements
|
||||
|
||||
These rules apply to all Java code written or modified in this repository.
|
||||
|
||||
## Nullability
|
||||
|
||||
- Annotate every class with JetBrains Annotations `@NotNullByDefault`.
|
||||
- Any type, field, parameter, return value, local variable, or generic type argument that may be `null` must be explicitly annotated with `@Nullable`.
|
||||
- Nullability must never be implicit.
|
||||
|
||||
## Immutability
|
||||
|
||||
- Immutable arrays and collections must be explicitly annotated with JetBrains Annotations `@Unmodifiable` or `@UnmodifiableView` as appropriate.
|
||||
- For arrays, use type-use syntax such as `String @Unmodifiable []`.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Every class, field, and method must have documentation.
|
||||
- Documentation must use `///` Markdown-style Javadoc comments.
|
||||
- Keep documentation accurate and specific to the actual behavior, constraints, and side effects.
|
||||
- Add concise implementation comments inside complex logic whenever they materially improve readability or explain non-obvious behavior.
|
||||
|
||||
## Gradle
|
||||
|
||||
- When invoking Gradle in this repository, always set `GRADLE_USER_HOME` to the workspace-local `.gradle-user-home` directory.
|
||||
- Prefer commands such as `./gradlew -g .gradle-user-home ...` or the equivalent environment-variable-based configuration.
|
||||
- When running Gradle `test` tasks, use a higher timeout of ten minutes.
|
||||
@@ -56,7 +56,7 @@ dependencies {
|
||||
implementation(project(":HMCLCore"))
|
||||
implementation(project(":HMCLBoot"))
|
||||
implementation("libs:JFoenix")
|
||||
implementation(libs.twelvemonkeys.imageio.webp)
|
||||
implementation(libs.jwebp)
|
||||
implementation(libs.fxsvgimage)
|
||||
implementation(libs.java.info)
|
||||
implementation(libs.monet.fx)
|
||||
@@ -195,7 +195,7 @@ tasks.shadowJar {
|
||||
exclude("META-INF/services/javax.imageio.spi.ImageInputStreamSpi")
|
||||
|
||||
listOf(
|
||||
"aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*", "freebsd-*", "linux-*", "darwin-*",
|
||||
"aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*", "freebsd-*", "linux-*",
|
||||
"*-ppc", "*-ppc64le", "*-s390x", "*-armel",
|
||||
).forEach { exclude("com/sun/jna/$it/**") }
|
||||
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
//
|
||||
// Source code recreated from a .class file by IntelliJ IDEA
|
||||
// (powered by Fernflower decompiler)
|
||||
//
|
||||
|
||||
package com.jfoenix.skins;
|
||||
|
||||
import com.jfoenix.adapters.skins.ComboBoxListViewSkin;
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import com.jfoenix.controls.JFXComboBox;
|
||||
import com.jfoenix.transitions.CachedTransition;
|
||||
import javafx.animation.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.ComboBoxBase;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.transform.Scale;
|
||||
import javafx.util.Duration;
|
||||
|
||||
public class JFXComboBoxListViewSkin<T> extends ComboBoxListViewSkin<T> {
|
||||
protected final ObjectProperty<Paint> promptTextFill;
|
||||
private boolean invalid = true;
|
||||
private final StackPane customPane;
|
||||
private final StackPane line = new StackPane();
|
||||
private final StackPane focusedLine = new StackPane();
|
||||
private final Text promptText = new Text();
|
||||
private final double initScale = 0.05;
|
||||
private final Scale scale;
|
||||
private final Timeline linesAnimation;
|
||||
private ParallelTransition transition;
|
||||
private CachedTransition promptTextUpTransition;
|
||||
private CachedTransition promptTextDownTransition;
|
||||
private CachedTransition promptTextColorTransition;
|
||||
private final Scale promptTextScale;
|
||||
private Paint oldPromptTextFill;
|
||||
private final BooleanBinding usePromptText;
|
||||
|
||||
public JFXComboBoxListViewSkin(JFXComboBox<T> comboBox) {
|
||||
super(comboBox);
|
||||
this.scale = new Scale(this.initScale, 1.0F);
|
||||
this.linesAnimation = new Timeline(new KeyFrame(Duration.ZERO, new KeyValue(this.scale.xProperty(), this.initScale, Interpolator.EASE_BOTH), new KeyValue(this.focusedLine.opacityProperty(), 0, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(1.0F), new KeyValue(this.focusedLine.opacityProperty(), 1, Interpolator.EASE_BOTH)), new KeyFrame(Duration.millis(160.0F), new KeyValue(this.scale.xProperty(), 1, Interpolator.EASE_BOTH)));
|
||||
this.promptTextScale = new Scale(1.0F, 1.0F, 0.0F, 0.0F);
|
||||
this.promptTextFill = new SimpleObjectProperty(Color.valueOf("#B2B2B2"));
|
||||
this.usePromptText = Bindings.createBooleanBinding(this::usePromptText, ((JFXComboBox) this.getSkinnable()).valueProperty(), this.getSkinnable().promptTextProperty());
|
||||
this.getArrowButton().setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, null, null)));
|
||||
this.promptText.textProperty().bind(comboBox.promptTextProperty());
|
||||
this.promptText.fillProperty().bind(this.promptTextFill);
|
||||
this.promptText.getStyleClass().addAll("text", "prompt-text");
|
||||
this.promptText.getTransforms().add(this.promptTextScale);
|
||||
if (!comboBox.isLabelFloat()) {
|
||||
this.promptText.visibleProperty().bind(this.usePromptText);
|
||||
}
|
||||
|
||||
this.customPane = new StackPane();
|
||||
this.customPane.setMouseTransparent(true);
|
||||
this.customPane.getStyleClass().add("combo-box-button-container");
|
||||
this.customPane.backgroundProperty().bindBidirectional(this.getSkinnable().backgroundProperty());
|
||||
this.customPane.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, null, null)));
|
||||
this.customPane.getChildren().add(this.promptText);
|
||||
this.getChildren().add(0, this.customPane);
|
||||
StackPane.setAlignment(this.promptText, Pos.CENTER_LEFT);
|
||||
this.line.getStyleClass().add("input-line");
|
||||
this.focusedLine.getStyleClass().add("input-focused-line");
|
||||
this.getChildren().add(this.line);
|
||||
this.getChildren().add(this.focusedLine);
|
||||
this.line.setPrefHeight(1.0F);
|
||||
this.line.setTranslateY(1.0F);
|
||||
this.line.setManaged(false);
|
||||
this.line.setBackground(new Background(new BackgroundFill(((JFXComboBox) this.getSkinnable()).getUnFocusColor(), CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
if (this.getSkinnable().isDisabled()) {
|
||||
this.line.setBorder(new Border(new BorderStroke(((JFXComboBox) this.getSkinnable()).getUnFocusColor(), BorderStrokeStyle.DASHED, CornerRadii.EMPTY, new BorderWidths(1.0F))));
|
||||
this.line.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
}
|
||||
|
||||
this.focusedLine.setPrefHeight(2.0F);
|
||||
this.focusedLine.setTranslateY(0.0F);
|
||||
this.focusedLine.setBackground(new Background(new BackgroundFill(((JFXComboBox) this.getSkinnable()).getFocusColor(), CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
this.focusedLine.setOpacity(0.0F);
|
||||
this.focusedLine.getTransforms().add(this.scale);
|
||||
this.focusedLine.setManaged(false);
|
||||
if (comboBox.isEditable()) {
|
||||
comboBox.getEditor().setStyle("-fx-background-color:TRANSPARENT;-fx-padding: 4 8 4 8");
|
||||
comboBox.getEditor().promptTextProperty().unbind();
|
||||
comboBox.getEditor().setPromptText(null);
|
||||
comboBox.getEditor().textProperty().addListener((o, oldVal, newVal) -> {
|
||||
this.usePromptText.invalidate();
|
||||
comboBox.setValue(this.getConverter().fromString(newVal));
|
||||
});
|
||||
}
|
||||
|
||||
comboBox.labelFloatProperty().addListener((o, oldVal, newVal) -> {
|
||||
if (newVal) {
|
||||
this.promptText.visibleProperty().unbind();
|
||||
JFXUtilities.runInFX(() -> this.createFloatingAnimation());
|
||||
} else {
|
||||
this.promptText.visibleProperty().bind(this.usePromptText);
|
||||
}
|
||||
|
||||
this.createFocusTransition();
|
||||
});
|
||||
comboBox.focusColorProperty().addListener((o, oldVal, newVal) -> {
|
||||
if (newVal != null) {
|
||||
this.focusedLine.setBackground(new Background(new BackgroundFill(newVal, CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
if (((JFXComboBox) this.getSkinnable()).isLabelFloat()) {
|
||||
this.promptTextColorTransition = new CachedTransition(this.customPane, new Timeline(new KeyFrame(Duration.millis(1300.0F), new KeyValue(this.promptTextFill, newVal, Interpolator.EASE_BOTH)))) {
|
||||
{
|
||||
this.setDelay(Duration.millis(0.0F));
|
||||
this.setCycleDuration(Duration.millis(160.0F));
|
||||
}
|
||||
|
||||
protected void starting() {
|
||||
super.starting();
|
||||
JFXComboBoxListViewSkin.this.oldPromptTextFill = JFXComboBoxListViewSkin.this.promptTextFill.get();
|
||||
}
|
||||
};
|
||||
this.transition = null;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
comboBox.unFocusColorProperty().addListener((o, oldVal, newVal) -> {
|
||||
if (newVal != null) {
|
||||
this.line.setBackground(new Background(new BackgroundFill(newVal, CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
}
|
||||
|
||||
});
|
||||
comboBox.disabledProperty().addListener((o, oldVal, newVal) -> {
|
||||
this.line.setBorder(newVal ? new Border(new BorderStroke(((JFXComboBox) this.getSkinnable()).getUnFocusColor(), BorderStrokeStyle.DASHED, CornerRadii.EMPTY, new BorderWidths(this.line.getHeight()))) : Border.EMPTY);
|
||||
this.line.setBackground(new Background(new BackgroundFill(newVal ? Color.TRANSPARENT : ((JFXComboBox) this.getSkinnable()).getUnFocusColor(), CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
});
|
||||
comboBox.focusedProperty().addListener((o, oldVal, newVal) -> {
|
||||
if (newVal) {
|
||||
this.focus();
|
||||
} else {
|
||||
this.unFocus();
|
||||
}
|
||||
|
||||
});
|
||||
comboBox.valueProperty().addListener((o, oldVal, newVal) -> {
|
||||
if (((JFXComboBox) this.getSkinnable()).isLabelFloat()) {
|
||||
this.animateFloatingLabel(newVal != null && !newVal.toString().isEmpty());
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
protected void layoutChildren(double x, double y, double w, double h) {
|
||||
super.layoutChildren(x, y, w, h);
|
||||
this.customPane.resizeRelocate(x, y, w, h);
|
||||
if (this.invalid) {
|
||||
this.invalid = false;
|
||||
if (!this.getSkinnable().isEditable()) {
|
||||
Text javaPromptText = (Text) super.getDisplayNode().lookup(".text");
|
||||
if (javaPromptText != null) {
|
||||
this.promptTextFill.set(javaPromptText.getFill());
|
||||
}
|
||||
}
|
||||
|
||||
this.createFloatingAnimation();
|
||||
if (((ComboBoxBase) this.getSkinnable()).getValue() != null) {
|
||||
this.animateFloatingLabel(true);
|
||||
}
|
||||
}
|
||||
|
||||
this.focusedLine.resizeRelocate(x, this.getSkinnable().getHeight(), w, this.focusedLine.prefHeight(-1.0F));
|
||||
this.line.resizeRelocate(x, this.getSkinnable().getHeight(), w, this.line.prefHeight(-1.0F));
|
||||
this.scale.setPivotX(w / (double) 2.0F);
|
||||
}
|
||||
|
||||
private void createFloatingAnimation() {
|
||||
this.promptTextUpTransition = new CachedTransition(this.customPane, new Timeline(new KeyFrame(Duration.millis(1300.0F), new KeyValue(this.promptText.translateYProperty(), -this.customPane.getHeight() + 6.05, Interpolator.EASE_BOTH), new KeyValue(this.promptTextScale.xProperty(), 0.85, Interpolator.EASE_BOTH), new KeyValue(this.promptTextScale.yProperty(), 0.85, Interpolator.EASE_BOTH)))) {
|
||||
{
|
||||
this.setDelay(Duration.millis(0.0F));
|
||||
this.setCycleDuration(Duration.millis(240.0F));
|
||||
}
|
||||
};
|
||||
this.promptTextColorTransition = new CachedTransition(this.customPane, new Timeline(new KeyFrame(Duration.millis(1300.0F), new KeyValue(this.promptTextFill, ((JFXComboBox) this.getSkinnable()).getFocusColor(), Interpolator.EASE_BOTH)))) {
|
||||
{
|
||||
this.setDelay(Duration.millis(0.0F));
|
||||
this.setCycleDuration(Duration.millis(160.0F));
|
||||
}
|
||||
|
||||
protected void starting() {
|
||||
super.starting();
|
||||
JFXComboBoxListViewSkin.this.oldPromptTextFill = JFXComboBoxListViewSkin.this.promptTextFill.get();
|
||||
}
|
||||
};
|
||||
this.promptTextDownTransition = new CachedTransition(this.customPane, new Timeline(new KeyFrame(Duration.millis(1300.0F), new KeyValue(this.promptText.translateYProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(this.promptTextScale.xProperty(), 1, Interpolator.EASE_BOTH), new KeyValue(this.promptTextScale.yProperty(), 1, Interpolator.EASE_BOTH)))) {
|
||||
{
|
||||
this.setDelay(Duration.millis(0.0F));
|
||||
this.setCycleDuration(Duration.millis(240.0F));
|
||||
}
|
||||
};
|
||||
this.promptTextDownTransition.setOnFinished((finish) -> {
|
||||
this.promptText.setTranslateY(0.0F);
|
||||
this.promptTextScale.setX(1.0F);
|
||||
this.promptTextScale.setY(1.0F);
|
||||
});
|
||||
}
|
||||
|
||||
private void focus() {
|
||||
if (this.transition == null) {
|
||||
this.createFocusTransition();
|
||||
}
|
||||
|
||||
this.transition.play();
|
||||
}
|
||||
|
||||
private void animateFloatingLabel(boolean up) {
|
||||
if (this.promptText == null) {
|
||||
Platform.runLater(() -> this.animateFloatingLabel(up));
|
||||
} else {
|
||||
if (this.transition != null) {
|
||||
this.transition.stop();
|
||||
this.transition.getChildren().remove(this.promptTextUpTransition);
|
||||
this.transition.getChildren().remove(this.promptTextColorTransition);
|
||||
this.transition = null;
|
||||
}
|
||||
|
||||
if (up && this.promptText.getTranslateY() == (double) 0.0F) {
|
||||
this.promptTextDownTransition.stop();
|
||||
this.promptTextUpTransition.play();
|
||||
if (this.getSkinnable().isFocused()) {
|
||||
this.promptTextColorTransition.play();
|
||||
}
|
||||
} else if (!up) {
|
||||
this.promptTextUpTransition.stop();
|
||||
if (this.getSkinnable().isFocused()) {
|
||||
this.promptTextFill.set(this.oldPromptTextFill);
|
||||
}
|
||||
|
||||
this.promptTextDownTransition.play();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void createFocusTransition() {
|
||||
this.transition = new ParallelTransition();
|
||||
if (((JFXComboBox) this.getSkinnable()).isLabelFloat()) {
|
||||
this.transition.getChildren().add(this.promptTextUpTransition);
|
||||
this.transition.getChildren().add(this.promptTextColorTransition);
|
||||
}
|
||||
|
||||
this.transition.getChildren().add(this.linesAnimation);
|
||||
}
|
||||
|
||||
private void unFocus() {
|
||||
if (this.transition != null) {
|
||||
this.transition.stop();
|
||||
}
|
||||
|
||||
this.scale.setX(this.initScale);
|
||||
this.focusedLine.setOpacity(0.0F);
|
||||
if (((JFXComboBox) this.getSkinnable()).isLabelFloat() && this.oldPromptTextFill != null) {
|
||||
this.promptTextFill.set(this.oldPromptTextFill);
|
||||
if (this.usePromptText()) {
|
||||
this.promptTextDownTransition.play();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private boolean usePromptText() {
|
||||
Object txt = ((JFXComboBox) this.getSkinnable()).getValue();
|
||||
String promptTxt = this.getSkinnable().getPromptText();
|
||||
return (txt == null || txt.toString().isEmpty()) && promptTxt != null && !promptTxt.isEmpty() && !this.promptTextFill.get().equals(Color.TRANSPARENT);
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ import org.jackhuang.hmcl.task.AsyncTaskExecutor;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.util.CrashReporter;
|
||||
@@ -138,6 +139,9 @@ public final class Launcher extends Application {
|
||||
Platform.setImplicitExit(false);
|
||||
Controllers.initialize(primaryStage);
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
|
||||
Themes.applyNativeDarkMode(primaryStage);
|
||||
|
||||
UpdateChecker.init();
|
||||
|
||||
primaryStage.show();
|
||||
|
||||
@@ -24,7 +24,6 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.EnumSet;
|
||||
|
||||
/**
|
||||
@@ -60,34 +59,38 @@ public final class Metadata {
|
||||
public static final String BUILD_CHANNEL = JarUtils.getAttribute("hmcl.version.type", "nightly");
|
||||
public static final String GITHUB_SHA = JarUtils.getAttribute("hmcl.version.hash", null);
|
||||
|
||||
public static final Path CURRENT_DIRECTORY = Paths.get(System.getProperty("user.dir")).toAbsolutePath().normalize();
|
||||
public static final Path CURRENT_DIRECTORY = Path.of(System.getProperty("user.dir")).toAbsolutePath().normalize();
|
||||
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
|
||||
public static final Path HMCL_GLOBAL_DIRECTORY;
|
||||
public static final Path HMCL_CURRENT_DIRECTORY;
|
||||
public static final Path DEPENDENCIES_DIRECTORY;
|
||||
|
||||
static {
|
||||
String hmclHome = System.getProperty("hmcl.home");
|
||||
if (hmclHome == null) {
|
||||
String hmclHome = System.getProperty("hmcl.home", System.getenv("HMCL_USER_HOME"));
|
||||
if (StringUtils.isBlank(hmclHome)) {
|
||||
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
|
||||
String xdgData = System.getenv("XDG_DATA_HOME");
|
||||
if (StringUtils.isNotBlank(xdgData)) {
|
||||
HMCL_GLOBAL_DIRECTORY = Paths.get(xdgData, "hmcl").toAbsolutePath().normalize();
|
||||
HMCL_GLOBAL_DIRECTORY = Path.of(xdgData, "hmcl").toAbsolutePath().normalize();
|
||||
} else {
|
||||
HMCL_GLOBAL_DIRECTORY = Paths.get(System.getProperty("user.home"), ".local", "share", "hmcl").toAbsolutePath().normalize();
|
||||
HMCL_GLOBAL_DIRECTORY = Path.of(System.getProperty("user.home"), ".local", "share", "hmcl").toAbsolutePath().normalize();
|
||||
}
|
||||
} else {
|
||||
HMCL_GLOBAL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
|
||||
}
|
||||
} else {
|
||||
HMCL_GLOBAL_DIRECTORY = Paths.get(hmclHome).toAbsolutePath().normalize();
|
||||
HMCL_GLOBAL_DIRECTORY = Path.of(hmclHome).toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
String hmclCurrentDir = System.getProperty("hmcl.dir");
|
||||
HMCL_CURRENT_DIRECTORY = hmclCurrentDir != null
|
||||
? Paths.get(hmclCurrentDir).toAbsolutePath().normalize()
|
||||
String hmclCurrentDir = System.getProperty("hmcl.dir", System.getenv("HMCL_LOCAL_HOME"));
|
||||
HMCL_CURRENT_DIRECTORY = StringUtils.isNotBlank(hmclCurrentDir)
|
||||
? Path.of(hmclCurrentDir).toAbsolutePath().normalize()
|
||||
: CURRENT_DIRECTORY.resolve(".hmcl");
|
||||
DEPENDENCIES_DIRECTORY = HMCL_CURRENT_DIRECTORY.resolve("dependencies");
|
||||
|
||||
String hmclDependencies = System.getProperty("hmcl.dependencies.dir", System.getenv("HMCL_DEPENDENCIES_DIR"));
|
||||
DEPENDENCIES_DIRECTORY = StringUtils.isNotBlank(hmclDependencies)
|
||||
? Path.of(hmclDependencies).toAbsolutePath().normalize()
|
||||
: HMCL_CURRENT_DIRECTORY.resolve("dependencies");
|
||||
}
|
||||
|
||||
public static boolean isStable() {
|
||||
|
||||
@@ -430,6 +430,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
.setNativesDirType(vs.getNativesDirType())
|
||||
.setNativesDir(vs.getNativesDir())
|
||||
.setProcessPriority(vs.getProcessPriority())
|
||||
.setGraphicsBackend(vs.getGraphicsBackend())
|
||||
.setRenderer(vs.getRenderer())
|
||||
.setEnableDebugLogOutput(vs.isEnableDebugLogOutput())
|
||||
.setUseNativeGLFW(vs.isUseNativeGLFW())
|
||||
@@ -514,7 +515,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
FORBIDDEN_VERSION_IDS.contains(id.toLowerCase(Locale.ROOT)))
|
||||
return false;
|
||||
|
||||
return FileUtils.isNameValid(id);
|
||||
return FileUtils.isNameValidForJar(id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -72,7 +72,6 @@ import static org.jackhuang.hmcl.util.Lang.resolveException;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.platform.Platform.SYSTEM_PLATFORM;
|
||||
import static org.jackhuang.hmcl.util.platform.Platform.isCompatibleWithX86Java;
|
||||
|
||||
public final class LauncherHelper {
|
||||
|
||||
@@ -173,27 +172,27 @@ public final class LauncherHelper {
|
||||
}
|
||||
}),
|
||||
Task.composeAsync(() -> {
|
||||
Renderer renderer = setting.getRenderer();
|
||||
if (renderer != Renderer.DEFAULT && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.SYSTEM_VERSION);
|
||||
if (lib == null)
|
||||
return null;
|
||||
Path file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
|
||||
if (file.toAbsolutePath().toString().indexOf('=') >= 0) {
|
||||
LOG.warning("Invalid character '=' in the libraries directory path, unable to attach software renderer loader");
|
||||
return null;
|
||||
}
|
||||
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS
|
||||
|| !(setting.getRenderer() instanceof Renderer.Driver renderer)
|
||||
|| renderer.mesaDriverName() == null)
|
||||
return null;
|
||||
|
||||
String agent = FileUtils.getAbsolutePath(file) + "=" + renderer.name().toLowerCase(Locale.ROOT);
|
||||
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.SYSTEM_VERSION);
|
||||
if (lib == null)
|
||||
return null;
|
||||
Path file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
|
||||
if (file.toAbsolutePath().toString().indexOf('=') >= 0) {
|
||||
LOG.warning("Invalid character '=' in the libraries directory path, unable to attach software renderer loader");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (GameLibrariesTask.shouldDownloadLibrary(repository, version.get(), lib, integrityCheck)) {
|
||||
return new LibraryDownloadTask(dependencyManager, file, lib)
|
||||
.thenRunAsync(() -> javaAgents.add(agent));
|
||||
} else {
|
||||
javaAgents.add(agent);
|
||||
return null;
|
||||
}
|
||||
String agent = FileUtils.getAbsolutePath(file) + "=" + renderer.mesaDriverName();
|
||||
|
||||
if (GameLibrariesTask.shouldDownloadLibrary(repository, version.get(), lib, integrityCheck)) {
|
||||
return new LibraryDownloadTask(dependencyManager, file, lib)
|
||||
.thenRunAsync(() -> javaAgents.add(agent));
|
||||
} else {
|
||||
javaAgents.add(agent);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
@@ -232,6 +231,7 @@ public final class LauncherHelper {
|
||||
if (quickPlayOption != null) {
|
||||
launchOptionsBuilder.setQuickPlayOption(quickPlayOption);
|
||||
}
|
||||
|
||||
LaunchOptions launchOptions = launchOptionsBuilder.create();
|
||||
|
||||
LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModsDirectory(selectedVersion), 10));
|
||||
@@ -624,7 +624,7 @@ public final class LauncherHelper {
|
||||
break;
|
||||
case VANILLA_X86:
|
||||
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER
|
||||
&& isCompatibleWithX86Java()) {
|
||||
&& Platform.isSupportedTranslationX86_64()) {
|
||||
suggestions.add(i18n("launch.advice.vanilla_x86.translation"));
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -31,7 +31,6 @@ public final class Log {
|
||||
|
||||
private final String log;
|
||||
private Log4jLevel level;
|
||||
private boolean selected = false;
|
||||
|
||||
public Log(String log) {
|
||||
this.log = log;
|
||||
@@ -57,14 +56,6 @@ public final class Log {
|
||||
return level;
|
||||
}
|
||||
|
||||
public boolean isSelected() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
public void setSelected(boolean selected) {
|
||||
this.selected = selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return log;
|
||||
|
||||
@@ -122,16 +122,27 @@ public final class JavaManager {
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case WINDOWS:
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
|
||||
// Windows x86-64 platform is compatible with x86 programs
|
||||
return architecture == Architecture.X86;
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
|
||||
return OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277 && architecture == Architecture.X86_64 || architecture == Architecture.X86;
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
|
||||
// Since Windows 10 Build 21277, Windows Arm64 has been compatible with x86-64 programs via translation
|
||||
if (architecture == Architecture.X86_64 && Platform.isSupportedTranslationX86_64())
|
||||
return true;
|
||||
|
||||
// Windows Arm64 is compatible with x86 programs via translation
|
||||
if (architecture == Architecture.X86)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case LINUX:
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
|
||||
return architecture == Architecture.X86;
|
||||
break;
|
||||
case MACOS:
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
|
||||
// macOS Arm64 compatible with x86-64 programs via Rosetta 2.
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64 && Platform.isSupportedTranslationX86_64())
|
||||
return architecture == Architecture.X86_64;
|
||||
break;
|
||||
}
|
||||
@@ -364,13 +375,13 @@ public final class JavaManager {
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
|
||||
searcher.searchAllJavaInRepository(Platform.WINDOWS_X86);
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277)
|
||||
if (Platform.isSupportedTranslationX86_64())
|
||||
searcher.searchAllJavaInRepository(Platform.WINDOWS_X86_64);
|
||||
searcher.searchAllJavaInRepository(Platform.WINDOWS_X86);
|
||||
}
|
||||
break;
|
||||
case MACOS:
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64 && Platform.isSupportedTranslationX86_64())
|
||||
searcher.searchAllJavaInRepository(Platform.MACOS_X86_64);
|
||||
break;
|
||||
}
|
||||
@@ -784,13 +795,15 @@ public final class JavaManager {
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
|
||||
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
|
||||
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277) {
|
||||
if (Platform.isSupportedTranslationX86_64()) {
|
||||
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.WINDOWS_X86_64), verify);
|
||||
}
|
||||
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
|
||||
}
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && Architecture.CURRENT_ARCH == Architecture.ARM64) {
|
||||
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.MACOS_X86_64), verify);
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64 && Platform.isSupportedTranslationX86_64()) {
|
||||
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.MACOS_X86_64), verify);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -603,6 +603,20 @@ public final class VersionSetting implements Cloneable, Observable {
|
||||
processPriorityProperty.set(processPriority);
|
||||
}
|
||||
|
||||
private final ObjectProperty<GraphicsAPI> graphicsBackend = new SimpleObjectProperty<>(this, "graphicsBackend", GraphicsAPI.DEFAULT);
|
||||
|
||||
public ObjectProperty<GraphicsAPI> graphicsBackendProperty() {
|
||||
return graphicsBackend;
|
||||
}
|
||||
|
||||
public GraphicsAPI getGraphicsBackend() {
|
||||
return graphicsBackendProperty().get();
|
||||
}
|
||||
|
||||
public void setGraphicsBackend(GraphicsAPI api) {
|
||||
graphicsBackendProperty().set(api);
|
||||
}
|
||||
|
||||
private final ObjectProperty<Renderer> rendererProperty = new SimpleObjectProperty<>(this, "renderer", Renderer.DEFAULT);
|
||||
|
||||
public Renderer getRenderer() {
|
||||
@@ -820,8 +834,9 @@ public final class VersionSetting implements Cloneable, Observable {
|
||||
}
|
||||
obj.addProperty("java", java);
|
||||
|
||||
obj.addProperty("graphicsBackend", src.getGraphicsBackend().name());
|
||||
obj.addProperty("renderer", src.getRenderer().name());
|
||||
if (src.getRenderer() == Renderer.LLVMPIPE)
|
||||
if (src.getRenderer() == Renderer.OpenGL.LLVMPIPE)
|
||||
obj.addProperty("useSoftwareRenderer", true);
|
||||
|
||||
return obj;
|
||||
@@ -894,16 +909,19 @@ public final class VersionSetting implements Cloneable, Observable {
|
||||
}
|
||||
|
||||
vs.setRenderer(Optional.ofNullable(obj.get("renderer")).map(JsonElement::getAsString)
|
||||
.map(Renderer::of).orElseGet(() -> {
|
||||
boolean useSoftwareRenderer = Optional.ofNullable(obj.get("useSoftwareRenderer")).map(JsonElement::getAsBoolean).orElse(false);
|
||||
return useSoftwareRenderer ? Renderer.OpenGL.LLVMPIPE : Renderer.DEFAULT;
|
||||
}));
|
||||
|
||||
vs.setGraphicsBackend(Optional.ofNullable(obj.get("graphicsBackend")).map(JsonElement::getAsString)
|
||||
.flatMap(name -> {
|
||||
try {
|
||||
return Optional.of(Renderer.valueOf(name.toUpperCase(Locale.ROOT)));
|
||||
return Optional.of(GraphicsAPI.valueOf(name.toUpperCase(Locale.ROOT)));
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}).orElseGet(() -> {
|
||||
boolean useSoftwareRenderer = Optional.ofNullable(obj.get("useSoftwareRenderer")).map(JsonElement::getAsBoolean).orElse(false);
|
||||
return useSoftwareRenderer ? Renderer.LLVMPIPE : Renderer.DEFAULT;
|
||||
}));
|
||||
}).orElseGet(() -> vs.getRenderer() instanceof Renderer.Driver renderer ? renderer.api() : GraphicsAPI.DEFAULT));
|
||||
|
||||
return vs;
|
||||
}
|
||||
|
||||
@@ -36,8 +36,9 @@ import org.glavo.monetfx.beans.property.ColorSchemeProperty;
|
||||
import org.glavo.monetfx.beans.property.ReadOnlyColorSchemeProperty;
|
||||
import org.glavo.monetfx.beans.property.SimpleColorSchemeProperty;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.ui.MacOSNativeUtils;
|
||||
import org.jackhuang.hmcl.ui.WindowsNativeUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.NativeUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OSVersion;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
@@ -233,6 +234,11 @@ public final class Themes {
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && MacOSNativeUtils.isSupported()) {
|
||||
MacOSNativeUtils.setAppearance(darkModeProperty().get());
|
||||
|
||||
ChangeListener<Boolean> listener = FXUtils.onWeakChange(Themes.darkModeProperty(), MacOSNativeUtils::setAppearance);
|
||||
stage.getProperties().put("Themes.applyNativeDarkMode.listener", listener);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -213,6 +213,21 @@ public final class Controllers {
|
||||
return decorator;
|
||||
}
|
||||
|
||||
public static void saveWindowStates() {
|
||||
if (stageX != null) {
|
||||
config().setX(stageX.get() / SCREEN.getBounds().getWidth());
|
||||
}
|
||||
if (stageY != null) {
|
||||
config().setY(stageY.get() / SCREEN.getBounds().getHeight());
|
||||
}
|
||||
if (stageHeight != null) {
|
||||
config().setHeight(stageHeight.get());
|
||||
}
|
||||
if (stageWidth != null) {
|
||||
config().setWidth(stageWidth.get());
|
||||
}
|
||||
}
|
||||
|
||||
public static void onApplicationStop() {
|
||||
stageSizeChangeListener = null;
|
||||
if (stageX != null) {
|
||||
@@ -375,7 +390,7 @@ public final class Controllers {
|
||||
Runnable continueAction = () -> globalConfig().setPlatformPromptVersion(1);
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
Controllers.dialog(i18n("fatal.unsupported_platform.macos_arm64"), null, MessageType.INFO, continueAction);
|
||||
continueAction.run();
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
Controllers.dialog(i18n("fatal.unsupported_platform.windows_arm64"), null, MessageType.INFO, continueAction);
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX &&
|
||||
|
||||
@@ -37,6 +37,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class HTMLRenderer {
|
||||
private static final String INDENT = " ";
|
||||
private static URI resolveLink(Node linkNode) {
|
||||
String href = linkNode.absUrl("href");
|
||||
if (href.isEmpty())
|
||||
@@ -57,6 +58,7 @@ public final class HTMLRenderer {
|
||||
private boolean underline;
|
||||
private boolean strike;
|
||||
private boolean highlight;
|
||||
private int indentLevel;
|
||||
private String headerLevel;
|
||||
private Node hyperlink;
|
||||
|
||||
@@ -218,7 +220,7 @@ public final class HTMLRenderer {
|
||||
appendImage(node);
|
||||
break;
|
||||
case "li":
|
||||
appendText("\n \u2022 ");
|
||||
appendText("\n" + INDENT.repeat(indentLevel) + " \u2022 ");
|
||||
break;
|
||||
case "dt":
|
||||
appendText(" ");
|
||||
@@ -237,11 +239,14 @@ public final class HTMLRenderer {
|
||||
}
|
||||
|
||||
if (node.childNodeSize() > 0) {
|
||||
boolean isLiNode = "li".equals(name);
|
||||
if (isLiNode) indentLevel++;
|
||||
pushNode(node);
|
||||
for (Node childNode : node.childNodes()) {
|
||||
appendNode(childNode);
|
||||
}
|
||||
popNode();
|
||||
if (isLiNode) indentLevel--;
|
||||
}
|
||||
|
||||
switch (name) {
|
||||
|
||||
@@ -30,7 +30,6 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.game.GameDumpGenerator;
|
||||
@@ -39,7 +38,6 @@ 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;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.util.CircularArrayList;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
@@ -170,6 +168,7 @@ public final class LogWindow extends Stage {
|
||||
LogWindowImpl() {
|
||||
getStyleClass().add("log-window");
|
||||
|
||||
listView.getStyleClass().add("no-horizontal-scrollbar");
|
||||
listView.getProperties().put("no-smooth-scrolling", true);
|
||||
listView.setItems(FXCollections.observableList(new CircularArrayList<>(logs.size())));
|
||||
|
||||
@@ -260,9 +259,6 @@ public final class LogWindow extends Stage {
|
||||
private static final PseudoClass INFO = PseudoClass.getPseudoClass("info");
|
||||
private static final PseudoClass DEBUG = PseudoClass.getPseudoClass("debug");
|
||||
private static final PseudoClass TRACE = PseudoClass.getPseudoClass("trace");
|
||||
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||
|
||||
private final Set<ListCell<Log>> selected = new HashSet<>();
|
||||
private final JFXSnackbar snackbar = new JFXSnackbar();
|
||||
|
||||
LogWindowSkin(LogWindowImpl control) {
|
||||
@@ -308,6 +304,7 @@ public final class LogWindow extends Stage {
|
||||
|
||||
{
|
||||
ListView<Log> listView = control.listView;
|
||||
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
listView.getItems().addListener((InvalidationListener) observable -> {
|
||||
if (!listView.getItems().isEmpty() && control.autoScroll.get())
|
||||
listView.scrollTo(listView.getItems().size() - 1);
|
||||
@@ -317,7 +314,6 @@ public final class LogWindow extends Stage {
|
||||
+ "\"; -fx-font-size: " + config().getFontSize() + "px;");
|
||||
listView.setCellFactory(x -> new ListCell<>() {
|
||||
{
|
||||
x.setSelectionModel(new NoneMultipleSelectionModel<>());
|
||||
getStyleClass().add("log-window-list-cell");
|
||||
Region clippedContainer = (Region) listView.lookup(".clipped-container");
|
||||
if (clippedContainer != null) {
|
||||
@@ -327,32 +323,6 @@ public final class LogWindow extends Stage {
|
||||
setPadding(new Insets(2));
|
||||
setWrapText(true);
|
||||
setGraphic(null);
|
||||
|
||||
setOnMouseClicked(event -> {
|
||||
if (event.getButton() != MouseButton.PRIMARY)
|
||||
return;
|
||||
|
||||
if (!event.isControlDown()) {
|
||||
for (ListCell<Log> logListCell : selected) {
|
||||
if (logListCell != this) {
|
||||
logListCell.pseudoClassStateChanged(SELECTED, false);
|
||||
if (logListCell.getItem() != null) {
|
||||
logListCell.getItem().setSelected(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selected.clear();
|
||||
}
|
||||
|
||||
selected.add(this);
|
||||
pseudoClassStateChanged(SELECTED, true);
|
||||
if (getItem() != null) {
|
||||
getItem().setSelected(true);
|
||||
}
|
||||
|
||||
event.consume();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -366,7 +336,6 @@ public final class LogWindow extends Stage {
|
||||
pseudoClassStateChanged(INFO, !empty && item.getLevel() == Log4jLevel.INFO);
|
||||
pseudoClassStateChanged(DEBUG, !empty && item.getLevel() == Log4jLevel.DEBUG);
|
||||
pseudoClassStateChanged(TRACE, !empty && item.getLevel() == Log4jLevel.TRACE);
|
||||
pseudoClassStateChanged(SELECTED, !empty && item.isSelected());
|
||||
|
||||
if (empty) {
|
||||
setText(null);
|
||||
@@ -378,10 +347,13 @@ public final class LogWindow extends Stage {
|
||||
|
||||
listView.setOnKeyPressed(event -> {
|
||||
if (event.isControlDown() && event.getCode() == KeyCode.C) {
|
||||
if (listView.getSelectionModel().isEmpty())
|
||||
return;
|
||||
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
|
||||
for (Log item : listView.getItems()) {
|
||||
if (item != null && item.isSelected()) {
|
||||
for (Log item : listView.getSelectionModel().getSelectedItems()) {
|
||||
if (item != null) {
|
||||
if (item.getLog() != null)
|
||||
stringBuilder.append(item.getLog());
|
||||
stringBuilder.append('\n');
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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 com.sun.jna.Pointer;
|
||||
import org.jackhuang.hmcl.util.platform.macos.ObjectiveCRuntime;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class MacOSNativeUtils {
|
||||
|
||||
private static final Pointer nsApp = init();
|
||||
|
||||
private static @Nullable Pointer init() {
|
||||
if (ObjectiveCRuntime.INSTANCE == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
var objc = ObjectiveCRuntime.INSTANCE;
|
||||
|
||||
Pointer nsApplication = objc.objc_getClass("NSApplication");
|
||||
if (!isNull(nsApplication)) {
|
||||
Pointer sharedSel = objc.sel_registerName("sharedApplication");
|
||||
if (!isNull(sharedSel))
|
||||
return objc.objc_msgSend(nsApplication, sharedSel);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to initialize macOS appearance support", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean isSupported() {
|
||||
return nsApp != null;
|
||||
}
|
||||
|
||||
private static boolean isNull(Pointer pointer) {
|
||||
return pointer == null || Pointer.nativeValue(pointer) == 0;
|
||||
}
|
||||
|
||||
public static void setAppearance(boolean dark) {
|
||||
setAppearance(dark, false);
|
||||
}
|
||||
|
||||
public static void setAppearance(boolean dark, boolean highContrast) {
|
||||
if (nsApp == null) return;
|
||||
|
||||
try {
|
||||
var objc = ObjectiveCRuntime.INSTANCE;
|
||||
|
||||
Pointer nsAppearance = objc.objc_getClass("NSAppearance");
|
||||
if (isNull(nsAppearance))
|
||||
return;
|
||||
|
||||
Pointer namedSel = objc.sel_registerName("appearanceNamed:");
|
||||
Pointer nsString = objc.objc_getClass("NSString");
|
||||
if (isNull(nsString)) return;
|
||||
|
||||
Pointer sel = objc.sel_registerName("stringWithUTF8String:");
|
||||
|
||||
String appearanceName;
|
||||
if (highContrast) {
|
||||
appearanceName = dark ? "NSAppearanceNameAccessibilityHighContrastDarkAqua" : "NSAppearanceNameAccessibilityHighContrastAqua";
|
||||
} else {
|
||||
appearanceName = dark ? "NSAppearanceNameDarkAqua" : "NSAppearanceNameAqua";
|
||||
}
|
||||
|
||||
Pointer appearanceNamePtr = objc.objc_msgSend(nsString, sel, appearanceName);
|
||||
if (isNull(appearanceNamePtr)) return;
|
||||
|
||||
Pointer appearance = objc.objc_msgSend(nsAppearance, namedSel, appearanceNamePtr);
|
||||
if (isNull(appearance)) return;
|
||||
|
||||
Pointer setSel = objc.sel_registerName("setAppearance:");
|
||||
objc.objc_msgSend(nsApp, setSel, appearance);
|
||||
} catch (Throwable t) {
|
||||
LOG.warning("Failed to set macOS appearance", t);
|
||||
}
|
||||
}
|
||||
|
||||
private MacOSNativeUtils() {
|
||||
}
|
||||
}
|
||||
@@ -164,6 +164,9 @@ public class AccountListItem extends RadioButton {
|
||||
if (skinImg.isError()) {
|
||||
throw new InvalidSkinException("Failed to read skin image", skinImg.getException());
|
||||
}
|
||||
if (skinImg.getWidth() != 64 || (skinImg.getHeight() != 32 && skinImg.getHeight() != 64)) {
|
||||
throw new InvalidSkinException("Invalid skin size");
|
||||
}
|
||||
NormalizedSkin skin = new NormalizedSkin(skinImg);
|
||||
String model = skin.isSlim() ? "slim" : "";
|
||||
LOG.info("Uploading skin [" + selectedFile + "], model [" + model + "]");
|
||||
|
||||
@@ -19,6 +19,8 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.animation.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
@@ -28,6 +30,7 @@ import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -43,8 +46,6 @@ final class ComponentSublistWrapper extends VBox implements NoPaddingComponent {
|
||||
private boolean expanded = false;
|
||||
|
||||
ComponentSublistWrapper(ComponentSublist sublist) {
|
||||
boolean noPadding = !sublist.hasComponentPadding();
|
||||
|
||||
this.getStyleClass().add("options-sublist-wrapper");
|
||||
|
||||
Node expandIcon = SVG.KEYBOARD_ARROW_DOWN.createIcon(20);
|
||||
@@ -104,12 +105,26 @@ final class ComponentSublistWrapper extends VBox implements NoPaddingComponent {
|
||||
|
||||
if (container == null) {
|
||||
this.container = new VBox();
|
||||
|
||||
if (!noPadding) {
|
||||
container.setPadding(new Insets(8, 16, 10, 16));
|
||||
}
|
||||
this.container.getStyleClass().add("container");
|
||||
FXUtils.setLimitHeight(container, 0);
|
||||
FXUtils.setOverflowHidden(container);
|
||||
|
||||
Rectangle rectangle = FXUtils.setOverflowHidden(container);
|
||||
rectangle.getStyleClass().add("overflow-hidden");
|
||||
|
||||
var last = PseudoClass.getPseudoClass("last");
|
||||
|
||||
InvalidationListener updateArc = o -> {
|
||||
if (ComponentSublistWrapper.this.getPseudoClassStates().contains(last)) {
|
||||
rectangle.setArcHeight(4);
|
||||
rectangle.setArcWidth(4);
|
||||
} else {
|
||||
rectangle.setArcHeight(0);
|
||||
rectangle.setArcWidth(0);
|
||||
}
|
||||
};
|
||||
updateArc.invalidated(null);
|
||||
ComponentSublistWrapper.this.getPseudoClassStates().addListener(updateArc);
|
||||
|
||||
container.getChildren().setAll(sublist);
|
||||
ComponentSublistWrapper.this.getChildren().add(container);
|
||||
|
||||
@@ -120,8 +135,7 @@ final class ComponentSublistWrapper extends VBox implements NoPaddingComponent {
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
// FIXME: ComponentSubList without padding must have a 4 pixel padding for displaying a border radius.
|
||||
double contentHeight = expanded ? (sublist.prefHeight(sublist.getWidth()) + (noPadding ? 4 : 8 + 10)) : 0;
|
||||
double contentHeight = expanded ? sublist.prefHeight(sublist.getWidth()) : 0;
|
||||
double targetRotate = expanded ? -180 : 0;
|
||||
|
||||
if (AnimationUtils.isAnimationEnabled()) {
|
||||
|
||||
@@ -17,8 +17,14 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.utils.JFXNodeUtils;
|
||||
import javafx.animation.Animation;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.NumberBinding;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.geometry.Point2D;
|
||||
import javafx.scene.Node;
|
||||
@@ -26,6 +32,10 @@ import javafx.scene.control.ScrollBar;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.animation.Motion;
|
||||
import org.jackhuang.hmcl.util.MathUtils;
|
||||
|
||||
// Referenced in root.css
|
||||
@@ -36,6 +46,10 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
|
||||
private Rectangle track = new Rectangle();
|
||||
private Rectangle thumb = new Rectangle();
|
||||
|
||||
@SuppressWarnings({"FieldCanBeLocal", "FieldMayBeFinal"})
|
||||
private ChangeListener<Boolean> thumbHoverListener;
|
||||
private Animation thumbHoverAnimation;
|
||||
|
||||
public FloatScrollBarSkin(final ScrollBar scrollBar) {
|
||||
this.scrollBar = scrollBar;
|
||||
scrollBar.setPrefHeight(1e-18);
|
||||
@@ -45,8 +59,8 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
|
||||
Point2D dragStart;
|
||||
double preDragThumbPos;
|
||||
|
||||
NumberBinding range = Bindings.subtract(scrollBar.maxProperty(), scrollBar.minProperty());
|
||||
NumberBinding position = Bindings.divide(Bindings.subtract(scrollBar.valueProperty(), scrollBar.minProperty()), range);
|
||||
final NumberBinding range = Bindings.subtract(scrollBar.maxProperty(), scrollBar.minProperty());
|
||||
final NumberBinding position = Bindings.divide(Bindings.subtract(scrollBar.valueProperty(), scrollBar.minProperty()), range);
|
||||
|
||||
{
|
||||
// Children are added unmanaged because for some reason the height of the bar keeps changing
|
||||
@@ -63,7 +77,6 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
|
||||
|
||||
setup();
|
||||
|
||||
|
||||
thumb.setOnMousePressed(me -> {
|
||||
if (me.isSynthesized()) {
|
||||
// touch-screen events handled by Scroll handler
|
||||
@@ -128,14 +141,18 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
|
||||
track.widthProperty().unbind();
|
||||
track.heightProperty().unbind();
|
||||
|
||||
double offset = 4;
|
||||
|
||||
if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
|
||||
track.relocate(0, -8);
|
||||
track.widthProperty().bind(scrollBar.widthProperty());
|
||||
track.setHeight(8);
|
||||
track.relocate(offset, -6.0);
|
||||
NumberBinding trackWidth = Bindings.max(0, Bindings.subtract(scrollBar.widthProperty(), offset * 2));
|
||||
track.widthProperty().bind(trackWidth);
|
||||
track.setHeight(6.0);
|
||||
} else {
|
||||
track.relocate(-8, 0);
|
||||
track.setWidth(8);
|
||||
track.heightProperty().bind(scrollBar.heightProperty());
|
||||
track.relocate(-6.0, offset);
|
||||
track.setWidth(6.0);
|
||||
NumberBinding trackHeight = Bindings.max(0, Bindings.subtract(scrollBar.heightProperty(), offset * 2));
|
||||
track.heightProperty().bind(trackHeight);
|
||||
}
|
||||
|
||||
thumb.xProperty().unbind();
|
||||
@@ -144,15 +161,37 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
|
||||
thumb.heightProperty().unbind();
|
||||
|
||||
if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
|
||||
thumb.relocate(0, -8);
|
||||
thumb.widthProperty().bind(Bindings.max(20, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.widthProperty())));
|
||||
thumb.setHeight(8);
|
||||
thumb.xProperty().bind(Bindings.subtract(scrollBar.widthProperty(), thumb.widthProperty()).multiply(position));
|
||||
thumb.relocate(0, -6.0);
|
||||
thumb.setHeight(6.0);
|
||||
|
||||
NumberBinding trackWidth = Bindings.max(0, Bindings.subtract(scrollBar.widthProperty(), offset * 2));
|
||||
|
||||
thumb.widthProperty().bind(Bindings.min(trackWidth,
|
||||
Bindings.max(20, scrollBar.visibleAmountProperty().divide(range).multiply(trackWidth))));
|
||||
|
||||
thumb.xProperty().bind(
|
||||
Bindings.add(offset,
|
||||
Bindings.multiply(
|
||||
Bindings.max(0, Bindings.subtract(trackWidth, thumb.widthProperty())),
|
||||
position)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
thumb.relocate(-8, 0);
|
||||
thumb.setWidth(8);
|
||||
thumb.heightProperty().bind(Bindings.max(20, scrollBar.visibleAmountProperty().divide(range).multiply(scrollBar.heightProperty())));
|
||||
thumb.yProperty().bind(Bindings.subtract(scrollBar.heightProperty(), thumb.heightProperty()).multiply(position));
|
||||
thumb.relocate(-6.0, 0);
|
||||
thumb.setWidth(6.0);
|
||||
|
||||
NumberBinding trackHeight = Bindings.max(0, Bindings.subtract(scrollBar.heightProperty(), offset * 2));
|
||||
|
||||
thumb.heightProperty().bind(Bindings.min(trackHeight,
|
||||
Bindings.max(20, scrollBar.visibleAmountProperty().divide(range).multiply(trackHeight))));
|
||||
|
||||
thumb.yProperty().bind(
|
||||
Bindings.add(offset,
|
||||
Bindings.multiply(
|
||||
Bindings.max(0, Bindings.subtract(trackHeight, thumb.heightProperty())),
|
||||
position)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +201,7 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
|
||||
return Double.MAX_VALUE;
|
||||
}
|
||||
|
||||
return 8;
|
||||
return 6.0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -171,13 +210,46 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
|
||||
return Double.MAX_VALUE;
|
||||
}
|
||||
|
||||
return 8;
|
||||
return 6.0;
|
||||
}
|
||||
};
|
||||
|
||||
this.thumbHoverListener = FXUtils.onWeakChangeAndOperate(thumb.hoverProperty(), newValue -> {
|
||||
if (thumbHoverAnimation != null) {
|
||||
thumbHoverAnimation.stop();
|
||||
thumbHoverAnimation = null;
|
||||
}
|
||||
|
||||
double targetOpacity = newValue ? 1.0 : 0.5;
|
||||
double currentOpacity = thumb.getOpacity();
|
||||
|
||||
double opacityAdjustment = targetOpacity - currentOpacity;
|
||||
if (Math.abs(opacityAdjustment) < 0.001) {
|
||||
if (opacityAdjustment != 0)
|
||||
thumb.setOpacity(targetOpacity);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (AnimationUtils.isAnimationEnabled() && JFXNodeUtils.isTreeVisible(thumb)) {
|
||||
thumbHoverAnimation = new Timeline(
|
||||
new KeyFrame(Duration.ZERO, new KeyValue(thumb.opacityProperty(), currentOpacity)),
|
||||
new KeyFrame(Motion.SHORT2, new KeyValue(thumb.opacityProperty(), targetOpacity))
|
||||
);
|
||||
thumbHoverAnimation.play();
|
||||
} else {
|
||||
thumb.setOpacity(targetOpacity);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
if (thumbHoverAnimation != null) {
|
||||
thumbHoverAnimation.stop();
|
||||
thumbHoverAnimation = null;
|
||||
}
|
||||
thumbHoverListener = null;
|
||||
scrollBar = null;
|
||||
group = null;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.StackPane;
|
||||
@@ -49,6 +50,8 @@ public class LineSelectButton<T> extends LineButton {
|
||||
private static final PseudoClass SELECTED_PSEUDO_CLASS = PseudoClass.getPseudoClass("selected");
|
||||
|
||||
private JFXPopup popup;
|
||||
@SuppressWarnings({"FieldCanBeLocal", "unused"})
|
||||
private ObservableList<Node> popupItems; // keep a reference
|
||||
|
||||
public LineSelectButton() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
@@ -87,7 +90,7 @@ public class LineSelectButton<T> extends LineButton {
|
||||
|
||||
ripplerContainer.addEventFilter(ScrollEvent.ANY, ignored -> popup.hide());
|
||||
|
||||
Bindings.bindContent(popupMenu.getContent(), MappedObservableList.create(itemsProperty(), item -> {
|
||||
Bindings.bindContent(popupMenu.getContent(), popupItems = MappedObservableList.create(itemsProperty(), item -> {
|
||||
VBox vbox = new VBox();
|
||||
|
||||
var itemTitleLabel = new Label();
|
||||
|
||||
@@ -19,8 +19,8 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
|
||||
public class MenuSeparator extends StackPane {
|
||||
|
||||
@@ -28,7 +28,7 @@ public class MenuSeparator extends StackPane {
|
||||
Rectangle rect = new Rectangle();
|
||||
rect.widthProperty().bind(widthProperty().add(-14));
|
||||
rect.setHeight(1);
|
||||
rect.setFill(Color.GRAY);
|
||||
rect.fillProperty().bind(Themes.colorSchemeProperty().getOutlineVariant());
|
||||
maxHeightProperty().set(10);
|
||||
setPadding(new Insets(3));
|
||||
getChildren().setAll(rect);
|
||||
|
||||
@@ -55,7 +55,8 @@ public final class MultiFileItem<T> extends VBox {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public MultiFileItem() {
|
||||
setPadding(new Insets(0, 0, 10, 0));
|
||||
this.getStyleClass().add("multi-file-item");
|
||||
|
||||
setSpacing(8);
|
||||
|
||||
group.selectedToggleProperty().addListener((a, b, newValue) -> {
|
||||
|
||||
@@ -81,12 +81,11 @@ public class TaskExecutorDialogPane extends BorderPane {
|
||||
|
||||
setCancel(cancel);
|
||||
|
||||
btnCancel.setDisable(onCancel.getCancellationAction() == null);
|
||||
btnCancel.setOnAction(e -> {
|
||||
if (onCancel.getCancellationAction() != null) {
|
||||
if (executor != null)
|
||||
executor.cancel();
|
||||
onCancel.getCancellationAction().accept(this);
|
||||
}
|
||||
if (executor != null)
|
||||
executor.cancel();
|
||||
onCancel.getCancellationAction().accept(this);
|
||||
});
|
||||
|
||||
speedEventHandler = FetchTask.SPEED_EVENT.registerWeak(speedEvent -> {
|
||||
|
||||
@@ -147,6 +147,10 @@ public class TwoLineListItem extends VBox {
|
||||
subtitleProperty().set(subtitle);
|
||||
}
|
||||
|
||||
public HBox getFirstLine() {
|
||||
return firstLine;
|
||||
}
|
||||
|
||||
public Label getTitleLabel() {
|
||||
return lblTitle;
|
||||
}
|
||||
@@ -168,6 +172,7 @@ public class TwoLineListItem extends VBox {
|
||||
Bindings.bindContent(tagsBox.getChildren(), tags);
|
||||
|
||||
var scrollPane = new ScrollPane(tagsBox);
|
||||
scrollPane.setMinSize(0, 0);
|
||||
HBox.setHgrow(scrollPane, Priority.ALWAYS);
|
||||
lblTitle.setMinWidth(Label.USE_PREF_SIZE);
|
||||
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
|
||||
@@ -353,12 +353,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
refreshNavButton.onActionProperty().bind(skinnable.onRefreshNavButtonActionProperty());
|
||||
skinnable.forbidDraggingWindow(refreshNavButton);
|
||||
|
||||
Rectangle separator = new Rectangle();
|
||||
separator.visibleProperty().bind(refreshNavButton.visibleProperty());
|
||||
separator.heightProperty().bind(navBar.heightProperty());
|
||||
separator.setFill(Color.GRAY);
|
||||
|
||||
navRight.getChildren().setAll(refreshNavButton, separator);
|
||||
navRight.getChildren().setAll(refreshNavButton);
|
||||
navBar.setRight(navRight);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
}
|
||||
}), i18n("message.downloading"), TaskCancellationAction.NORMAL);
|
||||
handler.resolve();
|
||||
}, file.getFile().getFilename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid), new Validator(i18n("profile.already_exists"), (it) -> !finalExistingFiles.contains(it)));
|
||||
}, file.getFile().getFilename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValidForJar), new Validator(i18n("profile.already_exists"), (it) -> !finalExistingFiles.contains(it)));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -423,7 +423,11 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
if (status == Status.LOADING)
|
||||
transitionPane.setContent(spinner, ContainerAnimations.FADE);
|
||||
else if (status == Status.SUCCESS)
|
||||
transitionPane.setContent(centerWrapper, ContainerAnimations.FADE);
|
||||
if (control.versions.isEmpty()) {
|
||||
transitionPane.setContent(emptyPane, ContainerAnimations.FADE);
|
||||
} else {
|
||||
transitionPane.setContent(centerWrapper, ContainerAnimations.FADE);
|
||||
}
|
||||
else // if (status == Status.FAILED)
|
||||
transitionPane.setContent(failedPane, ContainerAnimations.FADE);
|
||||
});
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.image;
|
||||
|
||||
import com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.SnapshotParameters;
|
||||
@@ -29,6 +28,9 @@ import org.girod.javafx.svgimage.LoaderParameters;
|
||||
import org.girod.javafx.svgimage.SVGImage;
|
||||
import org.girod.javafx.svgimage.SVGLoader;
|
||||
import org.girod.javafx.svgimage.ScaleQuality;
|
||||
import org.glavo.webp.WebPImage;
|
||||
import org.glavo.webp.WebPImageLoadOptions;
|
||||
import org.glavo.webp.javafx.WebPFXImage;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.image.apng.Png;
|
||||
import org.jackhuang.hmcl.ui.image.apng.argb8888.Argb8888Bitmap;
|
||||
@@ -38,13 +40,8 @@ import org.jackhuang.hmcl.ui.image.apng.chunks.PngFrameControl;
|
||||
import org.jackhuang.hmcl.ui.image.apng.error.PngException;
|
||||
import org.jackhuang.hmcl.ui.image.apng.error.PngIntegrityException;
|
||||
import org.jackhuang.hmcl.ui.image.internal.AnimationImageImpl;
|
||||
import org.jackhuang.hmcl.util.SwingFXUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageReader;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -71,16 +68,8 @@ public final class ImageUtils {
|
||||
};
|
||||
|
||||
public static final ImageLoader WEBP = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
|
||||
WebPImageReaderSpi spi = new WebPImageReaderSpi();
|
||||
ImageReader reader = spi.createReaderInstance(null);
|
||||
BufferedImage bufferedImage;
|
||||
try (ImageInputStream imageInput = ImageIO.createImageInputStream(input)) {
|
||||
reader.setInput(imageInput, true, true);
|
||||
bufferedImage = reader.read(0, reader.getDefaultReadParam());
|
||||
} finally {
|
||||
reader.dispose();
|
||||
}
|
||||
return SwingFXUtils.toFXImage(bufferedImage, requestedWidth, requestedHeight, preserveRatio, smooth);
|
||||
var options = new WebPImageLoadOptions(requestedWidth, requestedHeight, preserveRatio, smooth);
|
||||
return new WebPFXImage(WebPImage.read(input, options));
|
||||
};
|
||||
|
||||
public static final ImageLoader SVG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
|
||||
@@ -468,6 +457,19 @@ public final class ImageUtils {
|
||||
return new AnimationImageImpl(width, height, framePixels, durations, cycleCount);
|
||||
}
|
||||
|
||||
private static int[] rgbaToArgb(ByteBuffer rgba) {
|
||||
int pixelCount = rgba.remaining() / 4;
|
||||
int[] argb = new int[pixelCount];
|
||||
for (int i = 0; i < pixelCount; i++) {
|
||||
int r = rgba.get() & 0xFF;
|
||||
int g = rgba.get() & 0xFF;
|
||||
int b = rgba.get() & 0xFF;
|
||||
int a = rgba.get() & 0xFF;
|
||||
argb[i] = (a << 24) | (r << 16) | (g << 8) | b;
|
||||
}
|
||||
return argb;
|
||||
}
|
||||
|
||||
private ImageUtils() {
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.game.GraphicsAPI;
|
||||
import org.jackhuang.hmcl.game.NativesDirectoryType;
|
||||
import org.jackhuang.hmcl.game.Renderer;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
@@ -36,6 +37,7 @@ import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.nio.file.FileSystems;
|
||||
@@ -69,6 +71,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
private final ComponentSublist nativesDirSublist;
|
||||
private final MultiFileItem<NativesDirectoryType> nativesDirItem;
|
||||
private final MultiFileItem.FileOption<NativesDirectoryType> nativesDirCustomOption;
|
||||
private final LineSelectButton<GraphicsAPI> graphicsBackendPane;
|
||||
private final LineSelectButton<Renderer> rendererPane;
|
||||
|
||||
public AdvancedVersionSettingPage(Profile profile, @Nullable String versionId, VersionSetting versionSetting) {
|
||||
@@ -79,6 +82,10 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
versionId == null ? i18n("settings.advanced") : i18n("settings.advanced.title", versionId)
|
||||
));
|
||||
|
||||
@Nullable GameVersionNumber gameVersion = versionId != null
|
||||
? GameVersionNumber.asGameVersion(profile.getRepository().getGameVersion(versionId))
|
||||
: null;
|
||||
|
||||
this.getStyleClass().add("gray-background");
|
||||
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
@@ -181,9 +188,25 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
new MultiFileItem.Option<>(i18n("settings.advanced.natives_directory.default"), NativesDirectoryType.VERSION_FOLDER),
|
||||
nativesDirCustomOption
|
||||
));
|
||||
HintPane nativesDirHint = new HintPane(MessageDialogPane.MessageType.WARNING);
|
||||
nativesDirHint.setText(i18n("settings.advanced.natives_directory.hint"));
|
||||
nativesDirItem.getChildren().add(nativesDirHint);
|
||||
|
||||
graphicsBackendPane = new LineSelectButton<>();
|
||||
graphicsBackendPane.setTitle(i18n("settings.advanced.graphics_backend"));
|
||||
graphicsBackendPane.setConverter(backend -> i18n("settings.advanced.graphics_backend." + backend.name().toLowerCase(Locale.ROOT)));
|
||||
graphicsBackendPane.setDescriptionConverter(backend -> switch (backend) {
|
||||
case DEFAULT -> i18n("settings.advanced.graphics_backend.default.desc");
|
||||
case OPENGL -> i18n("settings.advanced.graphics_backend.opengl.desc");
|
||||
case VULKAN -> {
|
||||
if (gameVersion == null)
|
||||
yield i18n("settings.advanced.graphics_backend.vulkan.desc.global");
|
||||
else if (gameVersion.compareTo("26.2-snapshot-2") < 0)
|
||||
yield i18n("settings.advanced.graphics_backend.vulkan.desc.unsupported");
|
||||
else
|
||||
yield i18n("settings.advanced.graphics_backend.vulkan.desc");
|
||||
}
|
||||
default -> null;
|
||||
});
|
||||
graphicsBackendPane.setValue(GraphicsAPI.DEFAULT);
|
||||
graphicsBackendPane.setItems(GraphicsAPI.values());
|
||||
|
||||
rendererPane = new LineSelectButton<>();
|
||||
rendererPane.setTitle(i18n("settings.advanced.renderer"));
|
||||
@@ -192,7 +215,23 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
String bundleKey = "settings.advanced.renderer." + e.name().toLowerCase(Locale.ROOT) + ".desc";
|
||||
return I18n.hasKey(bundleKey) ? i18n(bundleKey) : null;
|
||||
});
|
||||
rendererPane.setItems(Renderer.values());
|
||||
rendererPane.setValue(Renderer.DEFAULT);
|
||||
|
||||
FXUtils.onChangeAndOperate(graphicsBackendPane.valueProperty(), backend -> {
|
||||
if (backend == null) { // unbind
|
||||
return;
|
||||
}
|
||||
|
||||
rendererPane.setItems(Renderer.getSupported(backend));
|
||||
if (backend == GraphicsAPI.DEFAULT) {
|
||||
rendererPane.setDisable(true);
|
||||
rendererPane.setValue(Renderer.DEFAULT);
|
||||
} else {
|
||||
rendererPane.setDisable(false);
|
||||
if (!(rendererPane.getValue() instanceof Renderer.Driver driver) || driver.api() != backend)
|
||||
rendererPane.setValue(Renderer.DEFAULT);
|
||||
}
|
||||
});
|
||||
|
||||
noJVMArgsPane = new LineToggleButton();
|
||||
noJVMArgsPane.setTitle(i18n("settings.advanced.no_jvm_args"));
|
||||
@@ -219,7 +258,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
useNativeOpenALPane.setSubtitle(i18n("settings.advanced.linux_freebsd_only"));
|
||||
|
||||
workaroundPane.getContent().setAll(
|
||||
nativesDirSublist, rendererPane, noJVMArgsPane, noOptimizingJVMArgsPane, noGameCheckPane,
|
||||
nativesDirSublist, graphicsBackendPane, rendererPane, noJVMArgsPane, noOptimizingJVMArgsPane, noGameCheckPane,
|
||||
noJVMCheckPane, noNativesPatchPane
|
||||
);
|
||||
|
||||
@@ -252,6 +291,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
FXUtils.bindString(txtPreLaunchCommand, versionSetting.preLaunchCommandProperty());
|
||||
FXUtils.bindString(txtPostExitCommand, versionSetting.postExitCommandProperty());
|
||||
rendererPane.valueProperty().bindBidirectional(versionSetting.rendererProperty());
|
||||
graphicsBackendPane.valueProperty().bindBidirectional(versionSetting.graphicsBackendProperty());
|
||||
noGameCheckPane.selectedProperty().bindBidirectional(versionSetting.notCheckGameProperty());
|
||||
noJVMCheckPane.selectedProperty().bindBidirectional(versionSetting.notCheckJVMProperty());
|
||||
noJVMArgsPane.selectedProperty().bindBidirectional(versionSetting.noJVMArgsProperty());
|
||||
@@ -294,6 +334,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
FXUtils.unbind(txtPreLaunchCommand, versionSetting.preLaunchCommandProperty());
|
||||
FXUtils.unbind(txtPostExitCommand, versionSetting.postExitCommandProperty());
|
||||
rendererPane.valueProperty().unbindBidirectional(versionSetting.rendererProperty());
|
||||
graphicsBackendPane.valueProperty().unbindBidirectional(versionSetting.graphicsBackendProperty());
|
||||
noGameCheckPane.selectedProperty().unbindBidirectional(versionSetting.notCheckGameProperty());
|
||||
noJVMCheckPane.selectedProperty().unbindBidirectional(versionSetting.notCheckJVMProperty());
|
||||
noJVMArgsPane.selectedProperty().unbindBidirectional(versionSetting.noJVMArgsProperty());
|
||||
|
||||
@@ -28,6 +28,7 @@ import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.transformation.FilteredList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -97,6 +98,14 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
|
||||
listView = new JFXListView<>();
|
||||
filteredList = new FilteredList<>(skinnable.getItems());
|
||||
|
||||
// reason for not using selectAll() is that selectAll() first clears all selected then selects all, causing the toolbar to flicker
|
||||
var selectAllButton = createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> listView.getSelectionModel().selectRange(0, listView.getItems().size()));
|
||||
|
||||
ListChangeListener<Object> listener = change -> {
|
||||
selectAllButton.setDisable(!listView.getItems().isEmpty()
|
||||
&& listView.getSelectionModel().getSelectedItems().size() == listView.getItems().size());
|
||||
};
|
||||
|
||||
{
|
||||
toolbarPane = new TransitionPane();
|
||||
searchBar = new HBox();
|
||||
@@ -123,12 +132,13 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
|
||||
enableButton.disableProperty().bind(getSkinnable().readOnly);
|
||||
disableButton.disableProperty().bind(getSkinnable().readOnly);
|
||||
|
||||
listView.getSelectionModel().getSelectedItems().addListener(listener);
|
||||
|
||||
selectingToolbar.getChildren().addAll(
|
||||
removeButton,
|
||||
enableButton,
|
||||
disableButton,
|
||||
createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () ->
|
||||
listView.getSelectionModel().selectRange(0, listView.getItems().size())),//reason for not using selectAll() is that selectAll() first clears all selected then selects all, causing the toolbar to flicker
|
||||
selectAllButton,
|
||||
createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () ->
|
||||
listView.getSelectionModel().clearSelection())
|
||||
);
|
||||
@@ -184,6 +194,7 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
|
||||
listView.setCellFactory(x -> new DataPackInfoListCell(listView, getSkinnable().readOnly));
|
||||
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
this.listView.setItems(filteredList);
|
||||
listView.getItems().addListener(listener);
|
||||
|
||||
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
|
||||
FXUtils.ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
||||
|
||||
@@ -531,7 +531,6 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
listView.setSelectionModel(new NoneMultipleSelectionModel<>());
|
||||
// ListViewBehavior would consume ESC pressed event, preventing us from handling it, so we ignore it here
|
||||
ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE);
|
||||
|
||||
listView.setCellFactory(x -> new ListCell<>() {
|
||||
private static final Insets PADDING = new Insets(9, 9, 0, 9);
|
||||
|
||||
@@ -578,7 +577,11 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
} else {
|
||||
ModTranslations.Mod mod = ModTranslations.getTranslationsByRepositoryType(getSkinnable().repository.getType()).getModByCurseForgeId(item.getSlug());
|
||||
content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : item.getTitle());
|
||||
content.setSubtitle(item.getDescription());
|
||||
String description = item.getDescription();
|
||||
if (description != null) {
|
||||
description = description.replaceAll("\\R", " ");
|
||||
}
|
||||
content.setSubtitle(description);
|
||||
content.getTags().clear();
|
||||
for (String category : item.getCategories()) {
|
||||
if (getSkinnable().shouldDisplayCategory(category))
|
||||
|
||||
@@ -225,8 +225,10 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
content.setSubtitle(getSkinnable().addon.getDescription());
|
||||
content.getSubtitleLabel().setWrapText(true);
|
||||
getSkinnable().addon.getCategories().stream()
|
||||
.filter(category -> getSkinnable().page.shouldDisplayCategory(category))
|
||||
.map(category -> getSkinnable().page.getLocalizedCategory(category))
|
||||
.forEach(content::addTag);
|
||||
content.getFirstLine().setMinWidth(0);
|
||||
descriptionPane.getChildren().add(content);
|
||||
|
||||
if (getSkinnable().mod != null) {
|
||||
|
||||
@@ -54,7 +54,10 @@ import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.FXThread;
|
||||
import org.jackhuang.hmcl.util.Lazy;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
@@ -154,6 +157,18 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
);
|
||||
|
||||
// Toolbar Selecting
|
||||
|
||||
// reason for not using selectAll() is that selectAll() first clears all selected then selects all, causing the toolbar to flicker
|
||||
var selectAll = createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> listView.getSelectionModel().selectRange(0, listView.getItems().size()));
|
||||
|
||||
ListChangeListener<Object> listener = change -> {
|
||||
selectAll.setDisable(!listView.getItems().isEmpty()
|
||||
&& listView.getSelectionModel().getSelectedItems().size() == listView.getItems().size());
|
||||
};
|
||||
|
||||
listView.getSelectionModel().getSelectedItems().addListener(listener);
|
||||
listView.getItems().addListener(listener);
|
||||
|
||||
toolbarSelecting.getChildren().setAll(
|
||||
createToolbarButton2(i18n("button.remove"), SVG.DELETE_FOREVER, () -> {
|
||||
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
|
||||
@@ -171,8 +186,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
.toList()
|
||||
)
|
||||
),
|
||||
createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () ->
|
||||
listView.getSelectionModel().selectAll()),
|
||||
selectAll,
|
||||
createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () ->
|
||||
listView.getSelectionModel().clearSelection())
|
||||
);
|
||||
|
||||
@@ -351,7 +351,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
cboWindowsSize.setPrefWidth(150);
|
||||
right.setLeft(cboWindowsSize);
|
||||
cboWindowsSize.setEditable(true);
|
||||
cboWindowsSize.setStyle("-fx-padding: 4 4 4 16");
|
||||
cboWindowsSize.setPromptText("854x480");
|
||||
cboWindowsSize.getItems().setAll(getSupportedResolutions());
|
||||
|
||||
|
||||
@@ -264,6 +264,11 @@ public final class Versions {
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.ps1"), "*.ps1"));
|
||||
Path file = FileUtils.toPath(chooser.showSaveDialog(Controllers.getStage()));
|
||||
if (file != null) {
|
||||
if (!isValidScriptExtension(FileUtils.getExtension(file))) {
|
||||
String defaultExt = getDefaultScriptExtension();
|
||||
file = file.resolveSibling(file.getFileName().toString() + "." + defaultExt);
|
||||
}
|
||||
|
||||
LauncherHelper launcherHelper = new LauncherHelper(profile, account, id);
|
||||
for (Consumer<LauncherHelper> injecter : injecters) {
|
||||
injecter.accept(launcherHelper);
|
||||
@@ -273,6 +278,21 @@ public final class Versions {
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean isValidScriptExtension(String ext) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
return ext.equalsIgnoreCase("bat") || ext.equalsIgnoreCase("ps1");
|
||||
}
|
||||
return ext.equalsIgnoreCase("sh") || ext.equalsIgnoreCase("bash") || ext.equalsIgnoreCase("command") || ext.equalsIgnoreCase("ps1");
|
||||
}
|
||||
|
||||
private static String getDefaultScriptExtension() {
|
||||
return switch (OperatingSystem.CURRENT_OS) {
|
||||
case WINDOWS -> "bat";
|
||||
case MACOS -> "command";
|
||||
default -> "sh";
|
||||
};
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static void launch(Profile profile, String id, Consumer<LauncherHelper>... injecters) {
|
||||
if (!checkVersionForLaunching(profile, id))
|
||||
|
||||
@@ -20,20 +20,20 @@ package org.jackhuang.hmcl.upgrade;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonParseException;
|
||||
import javafx.application.Platform;
|
||||
|
||||
import org.jackhuang.hmcl.EntryPoint;
|
||||
import org.jackhuang.hmcl.Main;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.UpgradeDialog;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.util.FileSaver;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.SwingUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -43,13 +43,15 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.checkFxUserThread;
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class UpdateHandler {
|
||||
private UpdateHandler() {
|
||||
@@ -125,6 +127,29 @@ public final class UpdateHandler {
|
||||
throw new IOException("Current JAR is not verified");
|
||||
}
|
||||
|
||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
|
||||
Platform.runLater(() -> {
|
||||
try {
|
||||
Controllers.saveWindowStates();
|
||||
} finally {
|
||||
future.complete(null);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
future.get();
|
||||
} catch (ExecutionException | InterruptedException ignored) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
FileSaver.waitForAllSaves();
|
||||
} catch (InterruptedException ignored) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
requestUpdate(downloaded, getCurrentLocation());
|
||||
EntryPoint.exit(0);
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -189,15 +189,16 @@ public final class NativePatcher {
|
||||
return version.setLibraries(newLibraries);
|
||||
}
|
||||
|
||||
public static @Nullable Library getWindowsMesaLoader(@NotNull JavaRuntime javaVersion, @NotNull Renderer renderer, @NotNull OSVersion windowsVersion) {
|
||||
/// @see <a href="https://github.com/HMCL-dev/mesa-loader-windows">Java Mesa Loader for Windows</a>
|
||||
public static @Nullable Library getWindowsMesaLoader(@NotNull JavaRuntime java, @NotNull Renderer renderer, @NotNull OSVersion windowsVersion) {
|
||||
if (renderer == Renderer.DEFAULT)
|
||||
return null;
|
||||
|
||||
if (windowsVersion.isAtLeast(OSVersion.WINDOWS_10)) {
|
||||
return getNatives(javaVersion.getPlatform()).get("mesa-loader");
|
||||
return getNatives(java.getPlatform()).get("mesa-loader");
|
||||
} else if (windowsVersion.isAtLeast(OSVersion.WINDOWS_7)) {
|
||||
if (renderer == Renderer.LLVMPIPE)
|
||||
return getNatives(javaVersion.getPlatform()).get("software-renderer-loader");
|
||||
if (renderer == Renderer.OpenGL.LLVMPIPE)
|
||||
return getNatives(java.getPlatform()).get("software-renderer-loader");
|
||||
else
|
||||
return null;
|
||||
} else {
|
||||
|
||||
@@ -96,6 +96,10 @@ public class Translator_lzh extends Translator {
|
||||
case GA -> {
|
||||
// do nothing
|
||||
}
|
||||
case SNAPSHOT -> {
|
||||
builder.append("之快照");
|
||||
appendDigitByDigit(builder, release.getEaVersion().toString());
|
||||
}
|
||||
case PRE_RELEASE -> {
|
||||
builder.append("之預");
|
||||
appendDigitByDigit(builder, release.getEaVersion().toString());
|
||||
|
||||
@@ -93,5 +93,10 @@
|
||||
"title": "LWJGL Unsafe Agent",
|
||||
"subtitle": "Copyright © 2026 Glavo.\nLicensed under the Apache 2.0 License.",
|
||||
"externalLink": "https://github.com/HMCL-dev/lwjgl-unsafe-agent"
|
||||
},
|
||||
{
|
||||
"title": "TomlJ",
|
||||
"subtitle": "Licensed under the Apache 2.0 License.",
|
||||
"externalLink": "https://github.com/tomlj/tomlj"
|
||||
}
|
||||
]
|
||||
@@ -29,8 +29,8 @@
|
||||
|
||||
.scroll-bar .thumb {
|
||||
-fx-fill: -monet-surface-tint;
|
||||
-fx-arc-width: 8;
|
||||
-fx-arc-height: 8;
|
||||
-fx-arc-width: 6;
|
||||
-fx-arc-height: 6;
|
||||
}
|
||||
|
||||
.title-label {
|
||||
@@ -414,7 +414,7 @@
|
||||
}
|
||||
|
||||
.installer-item-wrapper .installer-item:list-item > .installer-item-name {
|
||||
-fx-pref-width: 80px;
|
||||
-fx-pref-width: 100px;
|
||||
}
|
||||
|
||||
.installer-item-wrapper .installer-item:list-item > .installer-item-status {
|
||||
@@ -1093,8 +1093,12 @@
|
||||
-fx-background-color: -monet-error-container;
|
||||
}
|
||||
|
||||
.multi-file-item {
|
||||
-fx-padding: 10 0 10 0;
|
||||
}
|
||||
|
||||
.options-sublist {
|
||||
-fx-background-color: -monet-surface;
|
||||
-fx-background-color: -monet-surface-container;
|
||||
}
|
||||
|
||||
.options-list-item {
|
||||
@@ -1105,6 +1109,14 @@
|
||||
-fx-font-size: 12;
|
||||
}
|
||||
|
||||
.options-sublist .options-list-item {
|
||||
-fx-background-color: -monet-surface-container;
|
||||
}
|
||||
|
||||
.options-sublist .multi-file-item {
|
||||
-fx-padding: 10 10 10 10;
|
||||
}
|
||||
|
||||
.options-list-item .svg {
|
||||
-fx-fill: -monet-on-surface;
|
||||
-fx-border-color: -monet-outline-variant;
|
||||
@@ -1140,6 +1152,10 @@
|
||||
-fx-border-width: 0;
|
||||
}
|
||||
|
||||
.options-sublist-wrapper > .container {
|
||||
-fx-background-color: -monet-surface-container;
|
||||
}
|
||||
|
||||
.options-sublist-wrapper .expand-icon .svg {
|
||||
-fx-fill: -monet-on-surface-variant;
|
||||
}
|
||||
|
||||
@@ -363,16 +363,16 @@ download.failed.empty=No versions are available. Please click here to go back.
|
||||
download.failed.no_code=Failed to download
|
||||
download.failed.refresh=Failed to fetch version list. Please click here to retry.
|
||||
download.game=New Game
|
||||
download.provider.bmclapi=BMCLAPI
|
||||
download.provider.bmclapi.desc=bangbang93, https://bmclapi2.bangbang93.com/
|
||||
download.provider.bmclapi=BMCLAPI & MCIM
|
||||
download.provider.bmclapi.desc=This option is provided exclusively for users in Chinese Mainland.
|
||||
download.provider.mojang=Official
|
||||
download.provider.mojang.desc=OptiFine is provided by BMCLAPI
|
||||
download.provider.mojang.desc=Note that OptiFine is provided by BMCLAPI.
|
||||
download.provider.official=From Official Sources
|
||||
download.provider.official.desc=Latest, but may load slowly
|
||||
download.provider.official.desc=Always obtain from official sources.
|
||||
download.provider.balanced=From Fastest Available
|
||||
download.provider.balanced.desc=Balanced, but may not be the latest
|
||||
download.provider.balanced.desc=Best choice for ALL users.
|
||||
download.provider.mirror=From Mirror
|
||||
download.provider.mirror.desc=Fast, but may not be the latest
|
||||
download.provider.mirror.desc=Recommended option for users in Chinese Mainland.
|
||||
download.java=Downloading Java
|
||||
download.java.process=Java Download Process
|
||||
download.javafx=Downloading dependencies for the launcher...
|
||||
@@ -452,8 +452,6 @@ fatal.unsupported_platform=Minecraft is not fully supported on your platform yet
|
||||
If you cannot launch Minecraft 1.17 and later, you can try switching the "Renderer" to "Mesa LLVMpipe" in "Global/Instance-specific Settings → Advanced Settings" to use CPU rendering for better compatibility.
|
||||
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher has provided support for the Loongson platform.\n\
|
||||
If you encounter problems when playing a game, you can visit https://docs.hmcl.net/groups.html for help.
|
||||
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Launcher has provided support for the Apple silicon platform, using native ARM Java to launch games to get a smoother gaming experience.\n\
|
||||
If you encounter problems when playing a game, launching the game with Java based on x86-64 architecture may offer better compatibility.
|
||||
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher has provided native support for the Windows on Arm platform. If you encounter problems when playing a game, please try launching the game with Java based on x86 architecture.\n\
|
||||
\n\
|
||||
If you are using the <b>Qualcomm</b> platform, you may need to install the <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL Compatibility Pack</a> before playing games.\n\
|
||||
@@ -1337,6 +1335,15 @@ settings.advanced.dont_patch_natives=Do not attempt to automatically replace nat
|
||||
settings.advanced.environment_variables=Environment Variables
|
||||
settings.advanced.game_dir.default=Default (".minecraft/")
|
||||
settings.advanced.game_dir.independent=Isolated (".minecraft/versions/<instance name>/", except for assets and libraries)
|
||||
settings.advanced.graphics_backend=Graphics API
|
||||
settings.advanced.graphics_backend.default=Default
|
||||
settings.advanced.graphics_backend.default.desc=Follow the game's settings
|
||||
settings.advanced.graphics_backend.opengl=OpenGL
|
||||
settings.advanced.graphics_backend.opengl.desc=Use OpenGL for rendering
|
||||
settings.advanced.graphics_backend.vulkan=Vulkan
|
||||
settings.advanced.graphics_backend.vulkan.desc=Use Vulkan for rendering
|
||||
settings.advanced.graphics_backend.vulkan.desc.global=Use Vulkan for rendering (Since Minecraft 26.2)
|
||||
settings.advanced.graphics_backend.vulkan.desc.unsupported=Use Vulkan for rendering (not supported for the current game version)
|
||||
settings.advanced.java_permanent_generation_space=PermGen Space
|
||||
settings.advanced.java_permanent_generation_space.prompt=in MiB
|
||||
settings.advanced.jvm=JVM Options
|
||||
@@ -1356,10 +1363,6 @@ settings.advanced.natives_directory.choose=Choose the location of the desired na
|
||||
settings.advanced.natives_directory.custom=Custom
|
||||
settings.advanced.natives_directory.default=Default
|
||||
settings.advanced.natives_directory.default.version_id=<Instance Name>
|
||||
settings.advanced.natives_directory.hint=This option is intended only for users of Apple silicon or other not officially supported platforms. Please do not edit this option unless you know what you are doing.\n\
|
||||
\n\
|
||||
Before proceeding, please make sure all libraries (e.g. lwjgl.dll, libopenal.so) are provided in your desired directory.\n\
|
||||
Note: It is recommended to use a fully English-letters path for the specified local library file. Otherwise it may lead to game launch failure.
|
||||
settings.advanced.no_jvm_args=Do not add default JVM arguments
|
||||
settings.advanced.no_optimizing_jvm_args=Do not add default JVM optimization arguments
|
||||
settings.advanced.precall_command=Pre-launch Command
|
||||
@@ -1379,13 +1382,47 @@ settings.advanced.post_exit_command=Post-exit Command
|
||||
settings.advanced.post_exit_command.prompt=Commands to execute after the game exits
|
||||
settings.advanced.renderer=Renderer
|
||||
settings.advanced.renderer.default=Default
|
||||
settings.advanced.renderer.default.desc=OpenGL
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.d3d12.desc=DirectX 12 (Poor performance and compatibility)
|
||||
settings.advanced.renderer.default.desc=Use System Default
|
||||
# Vulkan Renderers
|
||||
settings.advanced.renderer.lavapipe=Mesa Lavapipe
|
||||
settings.advanced.renderer.lavapipe.desc=Software Vulkan Renderer
|
||||
settings.advanced.renderer.dozen=Mesa Dozen
|
||||
settings.advanced.renderer.dozen.desc=Vulkan renderer based on DirectX 12 (Experimental)
|
||||
settings.advanced.renderer.nvidia_vulkan=NVIDIA
|
||||
settings.advanced.renderer.nvidia_vulkan.desc=Official Vulkan driver for NVIDIA GPUs
|
||||
settings.advanced.renderer.nvidia_nvk=NVIDIA (NVK)
|
||||
settings.advanced.renderer.nvidia_nvk.desc=Open-source Vulkan driver for NVIDIA GPUs
|
||||
settings.advanced.renderer.amdvlk=AMD
|
||||
settings.advanced.renderer.amdvlk.desc=Official Vulkan driver for AMD GPUs
|
||||
settings.advanced.renderer.amd_radv=AMD (RADV)
|
||||
settings.advanced.renderer.amd_radv.desc=Open-source Vulkan driver for AMD GPUs
|
||||
settings.advanced.renderer.intel_vulkan=Intel
|
||||
settings.advanced.renderer.intel_vulkan.desc=Official Vulkan driver for Intel GPUs
|
||||
settings.advanced.renderer.intel_anv=Intel (ANV)
|
||||
settings.advanced.renderer.intel_anv.desc=Open-source Vulkan driver for Intel GPUs
|
||||
settings.advanced.renderer.intel_hasvk=Intel (HASVK)
|
||||
settings.advanced.renderer.intel_hasvk.desc=Open-source Vulkan driver for older Intel GPUs
|
||||
settings.advanced.renderer.qualcomm=Qualcomm
|
||||
settings.advanced.renderer.qualcomm.desc=Official Vulkan driver for Qualcomm Adreno GPUs
|
||||
settings.advanced.renderer.turnip=Qualcomm (Turnip)
|
||||
settings.advanced.renderer.turnip.desc=Open-source Vulkan driver for Qualcomm Adreno GPUs
|
||||
settings.advanced.renderer.moltenvk=MoltenVK
|
||||
settings.advanced.renderer.moltenvk.desc=Vulkan renderer based on Metal
|
||||
settings.advanced.renderer.kosmickrisp=KosmicKrisp
|
||||
settings.advanced.renderer.kosmickrisp.desc=Vulkan renderer based on Metal 4 (Experimental)
|
||||
settings.advanced.renderer.powervr=PowerVR
|
||||
settings.advanced.renderer.powervr.desc=Open-source Vulkan driver for PowerVR GPUs (Experimental)
|
||||
settings.advanced.renderer.panvk=PanVK
|
||||
settings.advanced.renderer.panvk.desc=Open-source Vulkan driver for Arm Mali GPUs (Experimental)
|
||||
settings.advanced.renderer.v3dv=V3DV
|
||||
settings.advanced.renderer.v3dv.desc=Open-source Vulkan driver for Raspberry Pi 4/5 GPUs (Experimental)
|
||||
# OpenGL Renderers
|
||||
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
|
||||
settings.advanced.renderer.llvmpipe.desc=Software (Poor performance, best compatibility)
|
||||
settings.advanced.renderer.llvmpipe.desc=Software OpenGL renderer
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.d3d12.desc=OpenGL renderer based on DirectX 12
|
||||
settings.advanced.renderer.zink=Mesa Zink
|
||||
settings.advanced.renderer.zink.desc=Vulkan (Best performance, poor compatibility)
|
||||
settings.advanced.renderer.zink.desc=OpenGL renderer based on Vulkan
|
||||
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
|
||||
|
||||
@@ -354,18 +354,11 @@ download.external_link=زيارة موقع التنزيل
|
||||
download.failed=فشل تنزيل "%1$s"، رمز الاستجابة: %2$d.
|
||||
download.failed.empty=لا توجد إصدارات متاحة. انقر هنا للرجوع.
|
||||
download.failed.no_code=فشل التنزيل
|
||||
download.failed.refresh=فشل جلب قائمة الإصدارات. انقر هنا لإعادة المحاولة.
|
||||
download.game=لعبة جديدة
|
||||
download.provider.bmclapi=BMCLAPI
|
||||
download.provider.bmclapi.desc=bangbang93, https://bmclapi2.bangbang93.com/
|
||||
download.provider.mojang=رسمي
|
||||
download.provider.mojang.desc=يتم توفير OptiFine عبر BMCLAPI
|
||||
download.failed.refresh=فشل في جلب قائمة الإصدارات. يرجى النقر هنا لإعادة المحاولة.
|
||||
download.game=New Game
|
||||
download.provider.official=من المصادر الرسمية
|
||||
download.provider.official.desc=الأحدث، لكن قد يكون التحميل بطيئاً
|
||||
download.provider.balanced=من أسرع المصادر المتاحة
|
||||
download.provider.balanced.desc=متوازن، لكن قد لا يكون الأحدث
|
||||
download.provider.mirror=من مرآة
|
||||
download.provider.mirror.desc=سريع، لكن قد لا يكون الأحدث
|
||||
download.java=جارٍ تنزيل Java
|
||||
download.java.process=عملية تنزيل Java
|
||||
download.javafx=جارٍ تنزيل مكتبات المشغّل...
|
||||
@@ -445,8 +438,6 @@ fatal.unsupported_platform=لا يدعم Minecraft منصتك الحالية ب
|
||||
إذا لم تتمكن من تشغيل Minecraft 1.17 أو أحدث، يمكنك محاولة تغيير "المُصيِّر" إلى "Mesa LLVMpipe" في "الإعدادات العامة/الخاصة بالنسخة ← الإعدادات المتقدمة" لاستخدام معالجة المعالج المركزي لتوافق أفضل.
|
||||
fatal.unsupported_platform.loongarch=وفّر Hello Minecraft! Launcher دعماً لمنصة Loongson.\n\
|
||||
إذا واجهت مشاكل أثناء اللعب، يمكنك زيارة https://docs.hmcl.net/groups.html للحصول على مساعدة.
|
||||
fatal.unsupported_platform.macos_arm64=وفّر Hello Minecraft! Launcher دعماً لمنصة Apple silicon، باستخدام Java ARM الأصلي لتشغيل الألعاب للحصول على تجربة لعب أكثر سلاسة.\n\
|
||||
إذا واجهت مشاكل أثناء اللعب، قد يوفر تشغيل اللعبة بـ Java المبني على معمارية x86-64 توافقاً أفضل.
|
||||
fatal.unsupported_platform.windows_arm64=وفّر Hello Minecraft! Launcher دعماً أصلياً لمنصة Windows on Arm. إذا واجهت مشاكل أثناء اللعب، يُرجى محاولة تشغيل اللعبة بـ Java المبني على معمارية x86.\n\
|
||||
\n\
|
||||
إذا كنت تستخدم منصة <b>Qualcomm</b>، قد تحتاج إلى تثبيت <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">حزمة توافق OpenGL</a> قبل اللعب.\n\
|
||||
@@ -1341,10 +1332,6 @@ settings.advanced.natives_directory.choose=اختر موقع المكتبة ال
|
||||
settings.advanced.natives_directory.custom=مخصص
|
||||
settings.advanced.natives_directory.default=افتراضي
|
||||
settings.advanced.natives_directory.default.version_id=<اسم النسخة>
|
||||
settings.advanced.natives_directory.hint=هذا الخيار مخصص فقط لمستخدمي Apple silicon والمنصات غير المدعومة رسمياً. يُرجى عدم تعديل هذا الخيار إلا إذا كنت تعرف ما تفعله.\n\
|
||||
\n\
|
||||
قبل المتابعة، يُرجى التأكد من توفر جميع المكتبات (مثل lwjgl.dll وlibopenal.so) في المجلد المحدد.\n\
|
||||
ملاحظة: يُنصح باستخدام مسار يحتوي على حروف إنجليزية فقط لملف المكتبة المحلية المحدد، وإلا قد يتسبب ذلك في فشل تشغيل اللعبة.
|
||||
settings.advanced.no_jvm_args=عدم إضافة وسيطات JVM الافتراضية
|
||||
settings.advanced.no_optimizing_jvm_args=عدم إضافة وسيطات تحسين JVM الافتراضية
|
||||
settings.advanced.precall_command=أمر ما قبل التشغيل
|
||||
@@ -1364,13 +1351,9 @@ settings.advanced.post_exit_command=أمر ما بعد الإغلاق
|
||||
settings.advanced.post_exit_command.prompt=أوامر تُنفَّذ بعد إغلاق اللعبة
|
||||
settings.advanced.renderer=المُصيِّر
|
||||
settings.advanced.renderer.default=افتراضي
|
||||
settings.advanced.renderer.default.desc=OpenGL
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.d3d12.desc=DirectX 12 (أداء وتوافق ضعيفان)
|
||||
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
|
||||
settings.advanced.renderer.llvmpipe.desc=برمجي (أداء ضعيف، توافق أفضل)
|
||||
settings.advanced.renderer.zink=Mesa Zink
|
||||
settings.advanced.renderer.zink.desc=Vulkan (أفضل أداء، توافق ضعيف)
|
||||
settings.advanced.server_ip=عنوان الخادم
|
||||
settings.advanced.server_ip.prompt=الانضمام تلقائياً بعد تشغيل اللعبة
|
||||
settings.advanced.unsupported_system_options=إعدادات غير مطبّقة على النظام الحالي
|
||||
|
||||
@@ -336,10 +336,6 @@ download.failed.empty=No hay versiones disponibles, por favor haga clic aquí pa
|
||||
download.failed.no_code=No se ha podido descargar
|
||||
download.failed.refresh=No se ha podido obtener la lista de versiones. Por favor, haga clic aquí para volver a intentarlo.
|
||||
download.game=Nuevo juego
|
||||
download.provider.bmclapi=BMCLAPI
|
||||
download.provider.bmclapi.desc=bangbang93, https://bmclapi2.bangbang93.com/
|
||||
download.provider.mojang=Oficial
|
||||
download.provider.mojang.desc=OptiFine es proporcionado por BMCLAPI
|
||||
download.provider.official=De fuentes oficiales
|
||||
download.provider.balanced=De la fuente más rápida disponible
|
||||
download.provider.mirror=Desde espejo
|
||||
@@ -418,8 +414,6 @@ fatal.unsupported_platform=Minecraft aún no es totalmente compatible con tu pla
|
||||
Si no puedes iniciar Minecraft 1.17 y versiones posteriores, puedes intentar cambiar el «Renderizador» a «Mesa LLVMpipe» en «Configuración global/específica de la instancia → Configuración avanzada» para utilizar el renderizado por CPU y mejorar la compatibilidad.
|
||||
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher ha prestado apoyo a la plataforma Loongson.\n\
|
||||
Si tienes problemas al jugar, puedes visitar https://docs.hmcl.net/groups.html para obtener ayuda.
|
||||
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Launcher ha proporcionado soporte para la plataforma de chips de Apple, utilizando Java nativo de ARM para ejecutar juegos y conseguir una experiencia de juego más fluida.\n\
|
||||
Si tienes problemas al jugar a un juego, ejecutarlo con Java de arquitectura x86-64 puede tener mejor compatibilidad.
|
||||
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher ha proporcionado soporte nativo para la plataforma Windows en Arm. Si tiene problemas al jugar a un juego, intente iniciarlo con Java de arquitectura x86.\n\
|
||||
\n\
|
||||
Si utilizas la plataforma <b>Qualcomm</b>, es posible que tengas que instalar el <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">paquete de compatibilidad OpenGL</a> antes de jugar.\n\
|
||||
@@ -1251,9 +1245,6 @@ settings.advanced.natives_directory.choose=Elija la ubicación de la biblioteca
|
||||
settings.advanced.natives_directory.custom=Personalizado
|
||||
settings.advanced.natives_directory.default=Por defecto
|
||||
settings.advanced.natives_directory.default.version_id=<Nombre de la instancia>
|
||||
settings.advanced.natives_directory.hint=Esta opción está pensada sólo para usuarios de chips de Apple u otras plataformas no soportadas oficialmente. Por favor, no modifique esta opción a menos que sepa lo que está haciendo.\n\
|
||||
\n\
|
||||
Antes de proceder, por favor, asegúrese de que todas las bibliotecas (por ejemplo, lwjgl.dll, libopenal.so) se proporcionan en su directorio deseado.
|
||||
settings.advanced.no_jvm_args=No añadir argumentos JVM por defecto
|
||||
settings.advanced.no_optimizing_jvm_args=No añadir argumentos de optimización de JVM
|
||||
settings.advanced.precall_command=Comando pre-lanzamiento:
|
||||
@@ -1268,13 +1259,9 @@ settings.advanced.post_exit_command=Comando post-salida
|
||||
settings.advanced.post_exit_command.prompt=El comando se ejecuta después de que el juego se detenga
|
||||
settings.advanced.renderer=Renderizador
|
||||
settings.advanced.renderer.default=Por defecto
|
||||
settings.advanced.renderer.default.desc=OpenGL
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.d3d12.desc=DirectX 12 (Rendimiento y compatibilidad deficientes)
|
||||
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
|
||||
settings.advanced.renderer.llvmpipe.desc=Software (Bajo rendimiento, máxima compatibilidad)
|
||||
settings.advanced.renderer.zink=Mesa Zink
|
||||
settings.advanced.renderer.zink.desc=Vulkan (Máximo rendimiento, baja compatibilidad)
|
||||
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
|
||||
|
||||
@@ -293,10 +293,6 @@ download.failed=%1$s のダウンロードに失敗しました、応答コー
|
||||
download.failed.empty=候補者なし。戻るにはここをクリックしてください。
|
||||
download.failed.refresh=バージョンリストをダウンロードできません。ここをクリックして再試行してください。
|
||||
download.game=ゲームのダウンロード
|
||||
download.provider.bmclapi=BMCLAPI
|
||||
download.provider.bmclapi.desc=ミラーソース
|
||||
download.provider.mojang=Mojang
|
||||
download.provider.mojang.desc=OptiFineはBMCLAPIにより提供されます
|
||||
download.provider.official=公式ソースからロード
|
||||
download.provider.balanced=mcbbsのソースからロード
|
||||
download.provider.mirror=ミラーソースからロード
|
||||
@@ -778,7 +774,6 @@ settings.advanced.natives_directory=ネイティブライブラリパス(例
|
||||
settings.advanced.natives_directory.choose=ネイティブライブラリパスを選択してください
|
||||
settings.advanced.natives_directory.custom=Custom(ゲームが実行時に必要とするネイティブライブラリを提供する必要があります)
|
||||
settings.advanced.natives_directory.default=Standard(ネイティブライブラリは当社が提供します)
|
||||
settings.advanced.natives_directory.hint=このオプションは、Appleシリコンまたはその他の公式にサポートされていないプラットフォーム/アーキテクチャでゲームをプレイする場合にのみ使用されます。\nこのオプションを変更する意味がわからない場合は、このオプションを変更しないことをお勧めします。、またはゲームが起動しません。\nこのオプションを変更する必要がある場合は、ゲームを実行するために必要なすべてのネイティブライブラリをカスタムネイティブディレクトリの下に配置する必要があります。lwjgl.dll(liblwjgl.so)、openal.dll(libopenal.so)。HMCLは、不足しているネイティブライブラリを提供しません。
|
||||
settings.advanced.no_jvm_args=デフォルトのJVM引数なし
|
||||
settings.advanced.precall_command=起動前コマンド
|
||||
settings.advanced.precall_command.prompt=ゲーム開始前に実行されます
|
||||
@@ -792,13 +787,9 @@ settings.advanced.post_exit_command=終了後のコマンド
|
||||
settings.advanced.post_exit_command.prompt=ゲーム終了後に実行されます
|
||||
settings.advanced.renderer=レンダラー
|
||||
settings.advanced.renderer.default=デフォルト
|
||||
settings.advanced.renderer.default.desc=OpenGL
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.d3d12.desc=DirectX 12 (低パフォーマンス、低互換性)
|
||||
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
|
||||
settings.advanced.renderer.llvmpipe.desc=ソフトウェア (低速だが、互換性は最高)
|
||||
settings.advanced.renderer.zink=Mesa Zink
|
||||
settings.advanced.renderer.zink.desc=Vulkan (最高パフォーマンス、低互換性)
|
||||
settings.advanced.server_ip=サーバーアドレス
|
||||
settings.advanced.server_ip.prompt=ゲームの起動時にサーバーに参加する
|
||||
settings.advanced.unsupported_system_options=サポートされていないシステムオプション
|
||||
|
||||
@@ -332,10 +332,6 @@ download.failed.empty=[無可裝之版,點此返]\n(君可求助於右上之
|
||||
download.failed.no_code=引之未成
|
||||
download.failed.refresh=[載版列未成,點此重試]\n(君可求助於右上之鈕)
|
||||
download.game=新戲
|
||||
download.provider.bmclapi=BMCLAPI
|
||||
download.provider.bmclapi.desc=bangbang93,https://bmclapi2.bangbang93.com
|
||||
download.provider.mojang=官版
|
||||
download.provider.mojang.desc=Optifine 猶取諸 BMCLAPI 也
|
||||
download.provider.official=先官版
|
||||
download.provider.official.desc=至新,容有艱
|
||||
download.provider.balanced=先其快者
|
||||
@@ -385,7 +381,6 @@ fatal.illegal_char=君之案夾名有諱文「=」,是以外載之戶簿、離
|
||||
fatal.unsupported_platform=礦藝未盡適君之算機,是以戲事或損,至於不能啟。\n\
|
||||
礦藝一點一七以晉之不能啟,或於「(全例/例殊) 戲設 → 進階置設 → 勘誤置設」改「繪製之器」以「Mesa LLVMpipe」,或能兼之。\n君可求助於右上之鈕。
|
||||
fatal.unsupported_platform.loongarch=HMCL 既適龍芯。\n凡有謬,遽求助於右上之鈕。
|
||||
fatal.unsupported_platform.macos_arm64=HMCL 既適蘋矽。宜啟以 ARM 之爪哇,以益君之戲事。\n誠有謬,宜啟以 x86-64 之爪哇,以益其兼。\n凡有謬,遽求助於右上之鈕。
|
||||
fatal.unsupported_platform.windows_arm64=HMCL 既適 ARM 之視窗。誠有謬,宜啟以 x86 之爪哇。\n誠用<b>栝柑</b>,或須置<a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">開圖庫兼囊</a>,而後可戯。擊鏈以置之於微軟貨舍。\n君可求助於右上之鈕。
|
||||
|
||||
file=案
|
||||
@@ -1066,7 +1061,6 @@ settings.advanced.natives_directory.choose=擇府庫之徑
|
||||
settings.advanced.natives_directory.custom=自定 (賴君所供之府庫也)
|
||||
settings.advanced.natives_directory.default=本 (賴啟者所供之府庫)
|
||||
settings.advanced.natives_directory.default.version_id=<例名>
|
||||
settings.advanced.natives_directory.hint=供於械網之弗見適者,有如蘋矽,以自定府庫也。微其之知,毋改是,改則或弗啟。\n\n將改,而宜錄府庫於是,蓋 lwjgl.dll (liblwjgl.so)、openal.dll (libopenal.so) 之類。啟者弗補是府庫也!\n\n誡:府庫之徑,但宜書以英文、數、下綫,否則或弗啟。
|
||||
settings.advanced.no_jvm_args=無添爪哇虛機之本通弦
|
||||
settings.advanced.precall_command=令於戲前
|
||||
settings.advanced.precall_command.prompt=將前戲啟而行
|
||||
@@ -1085,13 +1079,9 @@ settings.advanced.post_exit_command=令於戲訖
|
||||
settings.advanced.post_exit_command.prompt=將後戲訖而行
|
||||
settings.advanced.renderer=繪器
|
||||
settings.advanced.renderer.default=本
|
||||
settings.advanced.renderer.default.desc=開圖庫
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.d3d12.desc=DirectX 12 (效與所適皆差,勘誤之用)
|
||||
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
|
||||
settings.advanced.renderer.llvmpipe.desc=軟繪器 (效差,所適至)
|
||||
settings.advanced.renderer.zink=Mesa Zink
|
||||
settings.advanced.renderer.zink.desc=Vulkan (效至,所適差)
|
||||
settings.advanced.server_ip=伺服器之址
|
||||
settings.advanced.server_ip.prompt=本之址,啟訖徑入是伺服器
|
||||
settings.advanced.unsupported_system_options=不逮今之械網所選
|
||||
|
||||
@@ -333,10 +333,6 @@ download.failed.empty=Версий нет. Нажмите здесь, чтобы
|
||||
download.failed.no_code=Не удалось скачать
|
||||
download.failed.refresh=Не удалось получить список версий. Нажмите здесь, чтобы повторить попытку.
|
||||
download.game=Новая игра
|
||||
download.provider.bmclapi=BMCLAPI
|
||||
download.provider.bmclapi.desc=bangbang93, https://bmclapi2.bangbang93.com/
|
||||
download.provider.mojang=Официальный
|
||||
download.provider.mojang.desc=OptiFine предоставляется BMCLAPI
|
||||
download.provider.official=Из официальных источников
|
||||
download.provider.balanced=Из самых быстрых доступных
|
||||
download.provider.mirror=Из зеркала
|
||||
@@ -412,8 +408,6 @@ fatal.unsupported_platform=Minecraft еще не полностью поддер
|
||||
Если вы не можете запустить Minecraft версии 1.17 и новее, попробуйте переключить «Рендерер» на «Mesa LLVMpipe» в разделе «Глобальные/настройки экземпляра → Расширенные настройки», чтобы использовать рендеринг через CPU для лучшей совместимости.
|
||||
fatal.unsupported_platform.loongarch=Лаунчер обеспечил поддержку платформы Loongson.\n\
|
||||
Если у вас возникнут проблемы во время игры, вы можете обратиться за помощью на сайт https://docs.hmcl.net/groups.html.
|
||||
fatal.unsupported_platform.macos_arm64=Лаунчер обеспечил поддержку платформы Apple silicon, используя родную ARM Java для запуска игр, чтобы получить более плавный игровой опыт.\n\
|
||||
Если вы столкнулись с проблемами во время игры, запуск игры с Java на базе архитектуры x86-64 может обеспечить лучшую совместимость.
|
||||
fatal.unsupported_platform.windows_arm64=Лаунчер обеспечил нативную поддержку платформы Windows на архитектуре Arm. Если у вам возникли проблемы во время игры, попробуйте запустить игру с Java на базе архитектуры x86.\n\
|
||||
\n\
|
||||
Если вы используете платформу <b>Qualcomm</b>, вам может потребоваться установить <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">пакет совместимости OpenGL</a> перед началом игры.\n\
|
||||
@@ -1242,10 +1236,6 @@ settings.advanced.natives_directory.choose=Выберите расоложени
|
||||
settings.advanced.natives_directory.custom=Пользовательское
|
||||
settings.advanced.natives_directory.default=По умолчанию
|
||||
settings.advanced.natives_directory.default.version_id=<Название сборки>
|
||||
settings.advanced.natives_directory.hint=Параметр предназначен только для пользователей Apple silicon или других официально не поддерживаемых платформ. Не изменяйте, если не знаете что делаете.\n\
|
||||
\n\
|
||||
Прежде чем продолжить, убедитесь что все библиотеки (например, lwjgl.dll, libopenal.so) находятся в нужной папке.\n\
|
||||
Примечание: Рекомендуется использовать полностью английские пути для указанных файлов локальных библиотек, иначе игра может не запуститься.
|
||||
settings.advanced.no_jvm_args=Не добавлять аргументы JVM по умолчанию
|
||||
settings.advanced.precall_command=Команда перед запуском
|
||||
settings.advanced.precall_command.prompt=Команды, которые необходимо выполнить перед запуском игры
|
||||
@@ -1259,13 +1249,9 @@ settings.advanced.post_exit_command=Команда после выхода
|
||||
settings.advanced.post_exit_command.prompt=Команды, которые необходимо выполнить после выхода из игры
|
||||
settings.advanced.renderer=Рендерер
|
||||
settings.advanced.renderer.default=По умолчанию
|
||||
settings.advanced.renderer.default.desc=OpenGL
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.d3d12.desc=DirectX 12 (Низкая производительность и совместимость)
|
||||
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
|
||||
settings.advanced.renderer.llvmpipe.desc=ПО (Низкая производительность, лучшая совместимость)
|
||||
settings.advanced.renderer.zink=Mesa Zink
|
||||
settings.advanced.renderer.zink.desc=Vulkan (Лучшая производительность, низкая совместимость)
|
||||
settings.advanced.server_ip=Адрес сервера
|
||||
settings.advanced.server_ip.prompt=Присоединяться к серверу при запуске игры
|
||||
settings.advanced.unsupported_system_options=Настройки, не применимые к текущей системе
|
||||
|
||||
@@ -332,10 +332,6 @@ download.failed.empty=Немає доступних версій. Натисні
|
||||
download.failed.no_code=Не вдалося завантажити
|
||||
download.failed.refresh=Не вдалося отримати список версій. Натисніть тут, щоб повторити спробу.
|
||||
download.game=Нова гра
|
||||
download.provider.bmclapi=BMCLAPI
|
||||
download.provider.bmclapi.desc=bangbang93, https://bmclapi2.bangbang93.com/
|
||||
download.provider.mojang=Офіційне
|
||||
download.provider.mojang.desc=OptiFine надається BMCLAPI
|
||||
download.provider.official=З офіційних джерел
|
||||
download.provider.balanced=З найшвидшого доступного
|
||||
download.provider.mirror=З дзеркала
|
||||
@@ -398,8 +394,6 @@ fatal.unsupported_platform=Minecraft ще не повністю підтриму
|
||||
Якщо ви не можете запустити Minecraft версії 1.17 і новіші, спробуйте змінити «Рендерер» на «Mesa LLVMpipe» у розділі «Глобальні/налаштування екземпляра → Розширені налаштування», щоб використовувати рендеринг через CPU для кращої сумісності.
|
||||
fatal.unsupported_platform.loongarch=Hello Minecraft! Лаунчер надав підтримку платформи Loongson.\n\
|
||||
Якщо у вас виникли проблеми під час гри, ви можете відвідати https://docs.hmcl.net/groups.html для отримання допомоги.
|
||||
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Лаунчер надав підтримку платформи Apple silicon, використовуючи нативну ARM Java для запуску ігор для отримання плавнішого ігрового досвіду.\n\
|
||||
Якщо у вас виникли проблеми під час гри, запуск гри з Java на основі архітектури x86-64 може забезпечити кращу сумісність.
|
||||
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Лаунчер надав нативну підтримку платформи Windows on Arm. Якщо у вас виникли проблеми під час гри, спробуйте запустити гру з Java на основі архітектури x86.\n\
|
||||
Якщо ви використовуєте платформу <b>Qualcomm</b>, вам може знадобитися встановити <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">Пакет сумісності OpenGL</a> перед грою.\n\
|
||||
Натисніть посилання, щоб перейти до Microsoft Store та встановити пакет сумісності.
|
||||
@@ -1190,9 +1184,6 @@ settings.advanced.natives_directory.choose=Виберіть місце розт
|
||||
settings.advanced.natives_directory.custom=Власний
|
||||
settings.advanced.natives_directory.default=Типово
|
||||
settings.advanced.natives_directory.default.version_id=<Назва екземпляра>
|
||||
settings.advanced.natives_directory.hint=Ця опція призначена лише для користувачів Apple silicon або інших неофіційно підтримуваних платформ. Не редагуйте цю опцію, якщо ви не знаєте, що робите. \n\
|
||||
Перед продовженням переконайтеся, що всі бібліотеки (наприклад, lwjgl.dll, libopenal.so) надано у вашому бажаному каталозі. \n\
|
||||
Примітка: Рекомендується використовувати повністю англійські літери для вказаного локального файлу бібліотеки. Інакше це може призвести до невдалого запуску гри.
|
||||
settings.advanced.no_jvm_args=Не додавати типові аргументи JVM
|
||||
settings.advanced.precall_command=Команда перед запуском
|
||||
settings.advanced.precall_command.prompt=Команди для виконання перед запуском гри
|
||||
@@ -1206,13 +1197,9 @@ settings.advanced.post_exit_command=Команда після виходу
|
||||
settings.advanced.post_exit_command.prompt=Команди для виконання після виходу з гри
|
||||
settings.advanced.renderer=Рендерер
|
||||
settings.advanced.renderer.default=По умолчанию
|
||||
settings.advanced.renderer.default.desc=OpenGL
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.d3d12.desc=DirectX 12 (Погана продуктивність та сумісність)
|
||||
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
|
||||
settings.advanced.renderer.llvmpipe.desc=Програмний (Погана продуктивність, найкраща сумісність)
|
||||
settings.advanced.renderer.zink=Mesa Zink
|
||||
settings.advanced.renderer.zink.desc=Vulkan (Найкраща продуктивність, погана сумісність)
|
||||
settings.advanced.server_ip=Адреса сервера
|
||||
settings.advanced.server_ip.prompt=Автоматично приєднатися після запуску гри
|
||||
settings.advanced.unsupported_system_options=Налаштування, що не застосовуються до поточної системи
|
||||
|
||||
@@ -359,16 +359,16 @@ download.failed.empty=沒有可供安裝的版本,按一下此處返回。
|
||||
download.failed.no_code=下載失敗
|
||||
download.failed.refresh=載入版本清單失敗,按一下此處重試。
|
||||
download.game=新遊戲
|
||||
download.provider.bmclapi=BMCLAPI
|
||||
download.provider.bmclapi.desc=bangbang93, https://bmclapi2.bangbang93.com/
|
||||
download.provider.bmclapi=BMCLAPI & MCIM
|
||||
download.provider.bmclapi.desc=適用於中國大陸的鏡像源
|
||||
download.provider.mojang=官方伺服器
|
||||
download.provider.mojang.desc=OptiFine 由 BMCLAPI 提供
|
||||
download.provider.official=盡量使用官方源
|
||||
download.provider.official.desc=最新,但可能載入慢
|
||||
download.provider.balanced=選取載入速度快的下載源
|
||||
download.provider.balanced.desc=平衡,但可能不是最新
|
||||
download.provider.mojang.desc=注意:OptiFine 的下載由 BMCLAPI 提供
|
||||
download.provider.official=盡量使用官方來源
|
||||
download.provider.official.desc=優先從官方來源獲取資訊
|
||||
download.provider.balanced=選取載入速度快的下載來源
|
||||
download.provider.balanced.desc=根據你的網路連線狀況選用最佳來源
|
||||
download.provider.mirror=盡量使用鏡像源
|
||||
download.provider.mirror.desc=載入快,但可能不是最新
|
||||
download.provider.mirror.desc=推薦中國大陸使用者使用
|
||||
download.java=下載 Java
|
||||
download.java.process=下載 Java
|
||||
download.javafx=正在下載必要的執行時元件
|
||||
@@ -419,7 +419,6 @@ fatal.illegal_char=由於您的執行路徑中存在無效字元『=』,您將
|
||||
fatal.unsupported_platform=Minecraft 尚未對您的平臺提供完善支援,所以可能影響遊戲體驗或無法啟動遊戲。\n\
|
||||
若無法啟動 Minecraft 1.17 及更高版本,可以嘗試在「(全域/實例特定) 遊戲設定 → 進階設定 → 除錯選項」中將「繪製器」切換為「Mesa LLVMpipe」,以獲得更好的相容性。
|
||||
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher 已為龍芯提供支援。\n如果遇到問題,你可以點擊右上角幫助按鈕進行求助。
|
||||
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Launcher 已為 Apple Silicon 平臺提供支援。使用 ARM 原生 Java 啟動遊戲以獲得更流暢的遊戲體驗。\n如果你在遊戲中遭遇問題,使用 x86-64 架構的 Java 啟動遊戲可能有更好的相容性。
|
||||
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher 已為 Windows on Arm 平臺提供原生支援。如果你在遊戲中遭遇問題,請嘗試使用 x86 架構的 Java 啟動遊戲。\n\n如果你正在使用<b>高通</b>平臺,你可能需要安裝 <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL 相容包</a>後才能進行遊戲。點擊連結前往 Microsoft Store 安裝相容包。
|
||||
# When translating the following string into non-Latin languages, please retain the English translation
|
||||
fatal.wine_warning=HMCL is running under Wine or one of its distributions.\nAs a cross-platform application, HMCL is best experienced natively on your host OS.\nPress "OK" to continue, but please note that some issues may arise.\n\n您目前正在使用 Wine 或其衍生版本執行 HMCL。\nHMCL 是跨平臺的,我們建議您直接在主機系統上執行。\n按下「確定」以繼續,但請注意,此操作可能會引發一些問題。
|
||||
@@ -1127,6 +1126,15 @@ settings.advanced.dont_patch_natives=不嘗試自動取代本機庫
|
||||
settings.advanced.environment_variables=環境變數
|
||||
settings.advanced.game_dir.default=預設 (".minecraft/")
|
||||
settings.advanced.game_dir.independent=各實例獨立 (".minecraft/versions/<實例名>/",除 assets、libraries 外)
|
||||
settings.advanced.graphics_backend=圖形 API
|
||||
settings.advanced.graphics_backend.default=預設
|
||||
settings.advanced.graphics_backend.default.desc=遵循遊戲設定
|
||||
settings.advanced.graphics_backend.opengl=OpenGL
|
||||
settings.advanced.graphics_backend.opengl.desc=使用 OpenGL 渲染
|
||||
settings.advanced.graphics_backend.vulkan=Vulkan
|
||||
settings.advanced.graphics_backend.vulkan.desc=使用 Vulkan 渲染
|
||||
settings.advanced.graphics_backend.vulkan.desc.global=使用 Vulkan 渲染 (適用於 Minecraft 26.2 及更高版本)
|
||||
settings.advanced.graphics_backend.vulkan.desc.unsupported=使用 Vulkan 渲染 (不適用於當前遊戲版本)
|
||||
settings.advanced.java_permanent_generation_space=記憶體永久儲存區域
|
||||
settings.advanced.java_permanent_generation_space.prompt=單位 MiB
|
||||
settings.advanced.jvm=Java 虛擬機設定
|
||||
@@ -1146,7 +1154,6 @@ settings.advanced.natives_directory.choose=選取本機庫路徑
|
||||
settings.advanced.natives_directory.custom=自訂 (由你提供遊戲需要的本機庫)
|
||||
settings.advanced.natives_directory.default=預設 (由啟動器提供遊戲本機庫)
|
||||
settings.advanced.natives_directory.default.version_id=<實例名稱>
|
||||
settings.advanced.natives_directory.hint=本選項提供給 Apple Silicon 等未受遊戲官方支援的平台來自訂遊戲本機庫。如果你不知道本選項的含義,請你不要修改本選項,否則會導致遊戲無法啟動。\n\n如果你要修改本選項,你需要保證自訂目錄下有遊戲所需的本機庫檔案,如 lwjgl.dll (liblwjgl.so), openal.dll (libopenal.so) 等檔案。啟動器不會幫你補全缺少的本機庫檔案。\n\n注意:建議指定的本機庫檔案路徑使用全英文字元,否則可能導致遊戲啟動失敗。
|
||||
settings.advanced.no_jvm_args=不新增預設的 Java 虛擬機參數
|
||||
settings.advanced.no_optimizing_jvm_args=不自動新增 Java 虛擬機最佳化參數
|
||||
settings.advanced.precall_command=遊戲啟動前執行指令
|
||||
@@ -1164,15 +1171,49 @@ settings.advanced.process_priority.high=高
|
||||
settings.advanced.process_priority.high.desc=優先保證遊戲執行,但可能會導致其他程式卡頓
|
||||
settings.advanced.post_exit_command=遊戲結束後執行指令
|
||||
settings.advanced.post_exit_command.prompt=將在遊戲結束後呼叫使用
|
||||
settings.advanced.renderer=繪製器
|
||||
settings.advanced.renderer=繪製器/驅動
|
||||
settings.advanced.renderer.default=預設
|
||||
settings.advanced.renderer.default.desc=OpenGL
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.d3d12.desc=DirectX 12 (效能與相容性較差,用於除錯)
|
||||
settings.advanced.renderer.default.desc=使用系統預設繪製器
|
||||
# Vulkan Renderers
|
||||
settings.advanced.renderer.lavapipe=Mesa Lavapipe
|
||||
settings.advanced.renderer.lavapipe.desc=軟體 Vulkan 繪製器
|
||||
settings.advanced.renderer.dozen=Mesa Dozen
|
||||
settings.advanced.renderer.dozen.desc=基於 DirectX 12 的 Vulkan 繪製器 (實驗性)
|
||||
settings.advanced.renderer.nvidia_vulkan=NVIDIA
|
||||
settings.advanced.renderer.nvidia_vulkan.desc=NVIDIA GPU 的官方 Vulkan 驅動
|
||||
settings.advanced.renderer.nvidia_nvk=NVIDIA (NVK)
|
||||
settings.advanced.renderer.nvidia_nvk.desc=NVIDIA GPU 的開源 Vulkan 驅動
|
||||
settings.advanced.renderer.amdvlk=AMD
|
||||
settings.advanced.renderer.amdvlk.desc=AMD GPU 的官方 Vulkan 驅動
|
||||
settings.advanced.renderer.amd_radv=AMD (RADV)
|
||||
settings.advanced.renderer.amd_radv.desc=AMD GPU 的開源 Vulkan 驅動
|
||||
settings.advanced.renderer.intel_vulkan=Intel
|
||||
settings.advanced.renderer.intel_vulkan.desc=Intel GPU 的官方 Vulkan 驅動
|
||||
settings.advanced.renderer.intel_anv=Intel (ANV)
|
||||
settings.advanced.renderer.intel_anv.desc=Intel GPU 的開源 Vulkan 驅動
|
||||
settings.advanced.renderer.intel_hasvk=Intel (HASVK)
|
||||
settings.advanced.renderer.intel_hasvk.desc=Intel 舊 GPU 的開源 Vulkan 驅動
|
||||
settings.advanced.renderer.qualcomm=高通
|
||||
settings.advanced.renderer.qualcomm.desc=高通 Adreno GPU 的官方 Vulkan 驅動
|
||||
settings.advanced.renderer.turnip=高通 (Turnip)
|
||||
settings.advanced.renderer.turnip.desc=高通 Adreno GPU 的開源 Vulkan 驅動
|
||||
settings.advanced.renderer.moltenvk=MoltenVK
|
||||
settings.advanced.renderer.moltenvk.desc=基於 Metal 的 Vulkan 繪製器
|
||||
settings.advanced.renderer.kosmickrisp=KosmicKrisp
|
||||
settings.advanced.renderer.kosmickrisp.desc=基於 Metal 4 的 Vulkan 繪製器 (實驗性)
|
||||
settings.advanced.renderer.powervr=PowerVR
|
||||
settings.advanced.renderer.powervr.desc=PowerVR GPU 的開源 Vulkan 驅動 (實驗性)
|
||||
settings.advanced.renderer.panvk=PanVK
|
||||
settings.advanced.renderer.panvk.desc=Arm Mali GPU 的開源 Vulkan 驅動 (實驗性)
|
||||
settings.advanced.renderer.v3dv=V3DV
|
||||
settings.advanced.renderer.v3dv.desc=樹莓派 4/5 GPU 的開源 Vulkan 驅動 (實驗性)
|
||||
# OpenGL Renderers
|
||||
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
|
||||
settings.advanced.renderer.llvmpipe.desc=軟繪製器 (效能較差,相容性最好)
|
||||
settings.advanced.renderer.llvmpipe.desc=軟體 OpenGL 繪製器
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.d3d12.desc=基於 DirectX 12 的 OpenGL 繪製器
|
||||
settings.advanced.renderer.zink=Mesa Zink
|
||||
settings.advanced.renderer.zink.desc=Vulkan (效能最好,相容性較差)
|
||||
settings.advanced.renderer.zink.desc=基於 Vulkan 的 OpenGL 繪製器
|
||||
settings.advanced.server_ip=伺服器位址
|
||||
settings.advanced.server_ip.prompt=預設,啟動遊戲後直接進入對應伺服器
|
||||
settings.advanced.unsupported_system_options=不適用於目前系統的選項
|
||||
|
||||
@@ -361,10 +361,10 @@ download.failed.empty=[没有可供安装的版本,点击此处返回]\n(你
|
||||
download.failed.no_code=下载失败
|
||||
download.failed.refresh=[加载版本列表失败,点击此处重试]\n(你可以点击右上角帮助按钮进行求助)
|
||||
download.game=新游戏
|
||||
download.provider.bmclapi=BMCLAPI
|
||||
download.provider.bmclapi.desc=bangbang93, https://bmclapi2.bangbang93.com
|
||||
download.provider.bmclapi=BMCLAPI & MCIM
|
||||
download.provider.bmclapi.desc=适用于中国大陆的镜像源
|
||||
download.provider.mojang=官方
|
||||
download.provider.mojang.desc=OptiFine 自动安装使用 BMCLAPI 下载源
|
||||
download.provider.mojang.desc=注:OptiFine 自动安装使用 BMCLAPI 下载源
|
||||
download.provider.official=尽量使用官方源
|
||||
download.provider.official.desc=最新,但可能加载慢
|
||||
download.provider.balanced=选择加载速度快的下载源
|
||||
@@ -423,7 +423,6 @@ fatal.unsupported_platform=Minecraft 尚未对你的平台提供完善支持,
|
||||
若无法启动 Minecraft 1.17 及更高版本,可以尝试在“(全局/实例特定) 游戏设置 → 高级设置 → 调试选项”中将“渲染器”切换为“Mesa LLVMpipe”,以获得更好的兼容性。\n\
|
||||
如遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher 已为龙芯提供支持。\n如果遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Launcher 已为 Apple Silicon 平台提供支持。使用 ARM 原生 Java 启动游戏以获得更流畅的游戏体验。\n如果你在游戏中遇到问题,使用 x86-64 架构的 Java 启动游戏可能有更好的兼容性。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher 已为 Windows on Arm 平台提供原生支持。如果你在游戏中遇到问题,请尝试使用 x86 架构的 Java 启动游戏。\n如果你正在使用 <b>高通</b> 平台,你可能需要安装 <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL 兼容包</a> 后才能进行游戏。点击链接前往 Microsoft Store 安装兼容包。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
# When translating the following string into non-Latin languages, please retain the English translation
|
||||
fatal.wine_warning=HMCL is running under Wine or one of its distributions.\nAs a cross-platform application, HMCL is best experienced natively on your host OS.\nPress "OK" to continue, but please note that some issues may arise.\n\n您目前正在使用 Wine 或其发行版运行 HMCL。\nHMCL 是跨平台的,我们建议您直接在宿主系统上运行。\n按下“确定”以继续,但请注意,此操作可能会引发一些问题。
|
||||
@@ -1132,6 +1131,15 @@ settings.advanced.dont_patch_natives=不尝试自动替换本地库
|
||||
settings.advanced.environment_variables=环境变量
|
||||
settings.advanced.game_dir.default=默认 (".minecraft/")
|
||||
settings.advanced.game_dir.independent=各实例独立 (存放在 ".minecraft/versions/<实例名>/",除 assets、libraries 外)
|
||||
settings.advanced.graphics_backend=图形 API
|
||||
settings.advanced.graphics_backend.default=默认
|
||||
settings.advanced.graphics_backend.default.desc=遵循游戏设置
|
||||
settings.advanced.graphics_backend.opengl=OpenGL
|
||||
settings.advanced.graphics_backend.opengl.desc=使用 OpenGL 渲染
|
||||
settings.advanced.graphics_backend.vulkan=Vulkan
|
||||
settings.advanced.graphics_backend.vulkan.desc=使用 Vulkan 渲染
|
||||
settings.advanced.graphics_backend.vulkan.desc.global=使用 Vulkan 渲染 (适用于 Minecraft 26.2 及更高版本)
|
||||
settings.advanced.graphics_backend.vulkan.desc.unsupported=使用 Vulkan 渲染 (不适用于当前游戏版本)
|
||||
settings.advanced.java_permanent_generation_space=内存永久保存区域
|
||||
settings.advanced.java_permanent_generation_space.prompt=单位 MiB
|
||||
settings.advanced.jvm=Java 虚拟机设置
|
||||
@@ -1151,7 +1159,6 @@ settings.advanced.natives_directory.choose=选择本地库路径
|
||||
settings.advanced.natives_directory.custom=自定义 (由你提供游戏需要的本地库)
|
||||
settings.advanced.natives_directory.default=默认 (由启动器提供游戏本地库)
|
||||
settings.advanced.natives_directory.default.version_id=<实例名称>
|
||||
settings.advanced.natives_directory.hint=本选项提供给 Apple Silicon 等未受游戏官方支持的平台来自定义游戏本地库。如果你不知道本选项的含义,请不要修改本选项,否则会导致游戏无法启动!\n\n如果你要修改本选项,你需要保证自定义文件夹下有游戏所需的本地库文件,如 lwjgl.dll (liblwjgl.so)、openal.dll (libopenal.so) 等文件。启动器不会帮你补全缺少的本地库文件!\n\n注意:指定的本地库文件路径建议只包含英文大小写字母、数字和下划线,否则可能会导致启动游戏失败。
|
||||
settings.advanced.no_jvm_args=不添加默认的 Java 虚拟机参数
|
||||
settings.advanced.no_optimizing_jvm_args=不自动添加 Java 虚拟机优化参数
|
||||
settings.advanced.precall_command=游戏启动前执行命令
|
||||
@@ -1169,15 +1176,49 @@ settings.advanced.process_priority.high=高
|
||||
settings.advanced.process_priority.high.desc=优先保证游戏运行,但可能会导致其他程序卡顿
|
||||
settings.advanced.post_exit_command=游戏结束后执行命令
|
||||
settings.advanced.post_exit_command.prompt=将在游戏结束后调用
|
||||
settings.advanced.renderer=渲染器
|
||||
settings.advanced.renderer=渲染器/驱动
|
||||
settings.advanced.renderer.default=默认
|
||||
settings.advanced.renderer.default.desc=OpenGL
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.d3d12.desc=DirectX 12 (性能与兼容性较差,用于调试)
|
||||
settings.advanced.renderer.default.desc=使用系统默认渲染器
|
||||
# Vulkan Renderers
|
||||
settings.advanced.renderer.lavapipe=Mesa Lavapipe
|
||||
settings.advanced.renderer.lavapipe.desc=软件 Vulkan 渲染器
|
||||
settings.advanced.renderer.dozen=Mesa Dozen
|
||||
settings.advanced.renderer.dozen.desc=基于 DirectX 12 的 Vulkan 渲染器 (实验性)
|
||||
settings.advanced.renderer.nvidia_vulkan=NVIDIA
|
||||
settings.advanced.renderer.nvidia_vulkan.desc=NVIDIA GPU 的官方 Vulkan 驱动
|
||||
settings.advanced.renderer.nvidia_nvk=NVIDIA (NVK)
|
||||
settings.advanced.renderer.nvidia_nvk.desc=NVIDIA GPU 的开源 Vulkan 驱动
|
||||
settings.advanced.renderer.amdvlk=AMD
|
||||
settings.advanced.renderer.amdvlk.desc=AMD GPU 的官方 Vulkan 驱动
|
||||
settings.advanced.renderer.amd_radv=AMD (RADV)
|
||||
settings.advanced.renderer.amd_radv.desc=AMD GPU 的开源 Vulkan 驱动
|
||||
settings.advanced.renderer.intel_vulkan=Intel
|
||||
settings.advanced.renderer.intel_vulkan.desc=Intel GPU 的官方 Vulkan 驱动
|
||||
settings.advanced.renderer.intel_anv=Intel (ANV)
|
||||
settings.advanced.renderer.intel_anv.desc=Intel GPU 的开源 Vulkan 驱动
|
||||
settings.advanced.renderer.intel_hasvk=Intel (HASVK)
|
||||
settings.advanced.renderer.intel_hasvk.desc=Intel 旧 GPU 的开源 Vulkan 驱动
|
||||
settings.advanced.renderer.qualcomm=高通
|
||||
settings.advanced.renderer.qualcomm.desc=高通 Adreno GPU 的官方 Vulkan 驱动
|
||||
settings.advanced.renderer.turnip=高通 (Turnip)
|
||||
settings.advanced.renderer.turnip.desc=高通 Adreno GPU 的开源 Vulkan 驱动
|
||||
settings.advanced.renderer.moltenvk=MoltenVK
|
||||
settings.advanced.renderer.moltenvk.desc=基于 Metal 的 Vulkan 渲染器
|
||||
settings.advanced.renderer.kosmickrisp=KosmicKrisp
|
||||
settings.advanced.renderer.kosmickrisp.desc=基于 Metal 4 的 Vulkan 渲染器 (实验性)
|
||||
settings.advanced.renderer.powervr=PowerVR
|
||||
settings.advanced.renderer.powervr.desc=PowerVR GPU 的开源 Vulkan 驱动 (实验性)
|
||||
settings.advanced.renderer.panvk=PanVK
|
||||
settings.advanced.renderer.panvk.desc=Arm Mali GPU 的开源 Vulkan 驱动 (实验性)
|
||||
settings.advanced.renderer.v3dv=V3DV
|
||||
settings.advanced.renderer.v3dv.desc=树莓派 4/5 GPU 的开源 Vulkan 驱动 (实验性)
|
||||
# OpenGL Renderers
|
||||
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
|
||||
settings.advanced.renderer.llvmpipe.desc=软渲染器 (性能较差,兼容性最好)
|
||||
settings.advanced.renderer.llvmpipe.desc=软件 OpenGL 渲染器
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.d3d12.desc=基于 DirectX 12 的 OpenGL 渲染器
|
||||
settings.advanced.renderer.zink=Mesa Zink
|
||||
settings.advanced.renderer.zink.desc=Vulkan (性能最好,兼容性较差)
|
||||
settings.advanced.renderer.zink.desc=基于 Vulkan 的 OpenGL 渲染器
|
||||
settings.advanced.server_ip=服务器地址
|
||||
settings.advanced.server_ip.prompt=默认,启动游戏后可以直接进入对应服务器
|
||||
settings.advanced.unsupported_system_options=不适用于当前系统的选项
|
||||
|
||||
@@ -4403,13 +4403,13 @@
|
||||
},
|
||||
"windows-x86_64": {
|
||||
"mesa-loader": {
|
||||
"name": "org.glavo:mesa-loader-windows:25.0.3:x64",
|
||||
"name": "org.glavo:mesa-loader-windows:26.0.4:x64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-x64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-x64.jar",
|
||||
"sha1": "b2552fcc8c98e95b4559c39f2ff87cdb3aaaa513",
|
||||
"size": 27971214
|
||||
"path": "org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-x64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-x64.jar",
|
||||
"sha1": "f8da709c59ef61f531c91434ca0e3b4f39202981",
|
||||
"size": 49873846
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4427,13 +4427,13 @@
|
||||
},
|
||||
"windows-x86": {
|
||||
"mesa-loader": {
|
||||
"name": "org.glavo:mesa-loader-windows:25.0.3:x86",
|
||||
"name": "org.glavo:mesa-loader-windows:26.0.4:x86",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-x86.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-x86.jar",
|
||||
"sha1": "6fd0fd7da88ca6636f735e1e0f6feb7f62f59715",
|
||||
"size": 23311299
|
||||
"path": "org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-x86.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-x86.jar",
|
||||
"sha1": "ac6afaa8baa7c17468267c09e77e1296ee92d5ed",
|
||||
"size": 41742113
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4816,13 +4816,13 @@
|
||||
"com.mojang:text2speech:1.12.4:natives": null,
|
||||
"com.mojang:text2speech:1.13.9:natives-windows": null,
|
||||
"mesa-loader": {
|
||||
"name": "org.glavo:mesa-loader-windows:25.0.3:arm64",
|
||||
"name": "org.glavo:mesa-loader-windows:26.0.4:arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-arm64.jar",
|
||||
"sha1": "40ca5de3351bf48b7b9efa5d2598d5b6b4d1ab8e",
|
||||
"size": 24905270
|
||||
"path": "org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-arm64.jar",
|
||||
"sha1": "5c761a344700a07eaded51c5cf0cde36ee614706",
|
||||
"size": 43504189
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ dependencies {
|
||||
api(libs.kala.compress.tar)
|
||||
api(libs.simple.png.javafx)
|
||||
api(libs.gson)
|
||||
api(libs.toml)
|
||||
api(libs.tomlj)
|
||||
api(libs.xz)
|
||||
api(libs.lz4)
|
||||
api(libs.fx.gson)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/// The Graphics API.
|
||||
public enum GraphicsAPI {
|
||||
DEFAULT,
|
||||
OPENGL,
|
||||
VULKAN;
|
||||
|
||||
private final String minecraftArg = name().toLowerCase(Locale.ROOT);
|
||||
|
||||
public String getMinecraftArg() {
|
||||
return minecraftArg;
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,7 @@ public class LaunchOptions implements Serializable {
|
||||
private NativesDirectoryType nativesDirType;
|
||||
private String nativesDir;
|
||||
private ProcessPriority processPriority = ProcessPriority.NORMAL;
|
||||
private GraphicsAPI graphicsBackend = GraphicsAPI.DEFAULT;
|
||||
private Renderer renderer = Renderer.DEFAULT;
|
||||
private boolean useNativeGLFW;
|
||||
private boolean useNativeOpenAL;
|
||||
@@ -244,7 +245,11 @@ public class LaunchOptions implements Serializable {
|
||||
return processPriority;
|
||||
}
|
||||
|
||||
public Renderer getRenderer() {
|
||||
public @NotNull GraphicsAPI getGraphicsBackend() {
|
||||
return graphicsBackend;
|
||||
}
|
||||
|
||||
public @NotNull Renderer getRenderer() {
|
||||
return renderer;
|
||||
}
|
||||
|
||||
@@ -435,8 +440,13 @@ public class LaunchOptions implements Serializable {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRenderer(@NotNull Renderer renderer) {
|
||||
options.renderer = renderer;
|
||||
public Builder setGraphicsBackend(GraphicsAPI backend) {
|
||||
options.graphicsBackend = Objects.requireNonNullElse(backend, GraphicsAPI.DEFAULT);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRenderer(Renderer renderer) {
|
||||
options.renderer = Objects.requireNonNullElse(renderer, Renderer.DEFAULT);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,9 +17,529 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
public enum Renderer {
|
||||
DEFAULT,
|
||||
ZINK,
|
||||
LLVMPIPE,
|
||||
D3D12
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
import org.jackhuang.hmcl.util.platform.hardware.GraphicsCard;
|
||||
import org.jackhuang.hmcl.util.platform.hardware.HardwareVendor;
|
||||
import org.jackhuang.hmcl.util.platform.macos.HomebrewUtils;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// @author Glavo
|
||||
@NotNullByDefault
|
||||
public sealed interface Renderer permits Renderer.Default, Renderer.Driver, Renderer.Unknown {
|
||||
|
||||
/// Default renderer.
|
||||
Default DEFAULT = new Default();
|
||||
|
||||
/// Parse a renderer from a string.
|
||||
static Renderer of(String name) {
|
||||
if ("DEFAULT".equalsIgnoreCase(name) || name.isBlank())
|
||||
return DEFAULT;
|
||||
|
||||
String upper = name.toUpperCase(Locale.ROOT).trim();
|
||||
|
||||
try {
|
||||
return OpenGL.valueOf(upper);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
|
||||
try {
|
||||
return Vulkan.valueOf(upper);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
|
||||
return new Unknown(name);
|
||||
}
|
||||
|
||||
/// Get all supported renderers for a given graphics API.
|
||||
static @Unmodifiable List<Renderer> getSupported(GraphicsAPI api) {
|
||||
return switch (api) {
|
||||
case DEFAULT -> List.of(DEFAULT);
|
||||
case VULKAN -> Vulkan.Holder.SUPPORTED;
|
||||
case OPENGL -> OpenGL.SUPPORTED;
|
||||
};
|
||||
}
|
||||
|
||||
String name();
|
||||
|
||||
final class Default implements Renderer {
|
||||
private Default() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "DEFAULT";
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface Driver extends Renderer permits Vulkan, OpenGL {
|
||||
|
||||
/// The graphics API that this driver belongs to.
|
||||
GraphicsAPI api();
|
||||
|
||||
/// If this driver can be loaded via [mesa-loader-windows](https://github.com/HMCL-dev/mesa-loader-windows),
|
||||
/// return the driver name, otherwise return `null`.
|
||||
@Contract(pure = true)
|
||||
default @Nullable String mesaDriverName() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
enum Vulkan implements Driver {
|
||||
/// Mesa Lavapipe driver.
|
||||
///
|
||||
/// It is a software rasterizer.
|
||||
///
|
||||
/// @see <a href="https://docs.mesa3d.org/drivers/llvmpipe.html">LLVMpipe - The Mesa 3D Graphics Library</a>
|
||||
LAVAPIPE("lvp") {
|
||||
@Override
|
||||
public String mesaDriverName() {
|
||||
return "lavapipe";
|
||||
}
|
||||
},
|
||||
|
||||
/// Mesa Dozen driver.
|
||||
///
|
||||
/// It is a Vulkan driver based on DirectX 12.
|
||||
///
|
||||
/// ## Note
|
||||
/// Currently, Dozen does not support the VK_KHR_push_descriptor feature, so it cannot launch Minecraft 26.2
|
||||
/// Using Dozen can run Minecraft 1.21.11 + VulkanMod, but it will cause the game to crash after playing for a while
|
||||
DOZEN("dzn") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return platform.os() == OperatingSystem.WINDOWS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String mesaDriverName() {
|
||||
return "dzn";
|
||||
}
|
||||
},
|
||||
|
||||
/// NVIDIA Vulkan driver.
|
||||
///
|
||||
/// It is a Vulkan driver for NVIDIA GPUs.
|
||||
///
|
||||
/// @see <a href="https://developer.nvidia.com/vulkan">Vulkan Open Standard Modern GPU API | NVIDIA Developer</a>
|
||||
NVIDIA_VULKAN("nvidia") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return Vulkan.hasCard(cards, HardwareVendor.NVIDIA);
|
||||
}
|
||||
},
|
||||
|
||||
/// Mesa NVK driver.
|
||||
///
|
||||
/// It is a Vulkan driver for NVIDIA GPUs.
|
||||
///
|
||||
/// @see <a href="https://docs.mesa3d.org/drivers/nvk.html">NVK - The Mesa 3D Graphics Library</a>
|
||||
NVIDIA_NVK("nouveau") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return platform.os() == OperatingSystem.LINUX && Vulkan.hasCard(cards, HardwareVendor.NVIDIA);
|
||||
}
|
||||
},
|
||||
|
||||
/// AMD Open Source Driver for Vulkan
|
||||
///
|
||||
/// It is a Vulkan driver for AMD GPUs.
|
||||
///
|
||||
/// @see <a href="https://github.com/GPUOpen-Drivers/AMDVLK">GPUOpen-Drivers/AMDVLK - GitHub</a>
|
||||
AMDVLK("amd") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return platform.os() == OperatingSystem.LINUX && Vulkan.hasCard(cards, HardwareVendor.AMD);
|
||||
}
|
||||
},
|
||||
|
||||
/// Mesa RADV driver.
|
||||
///
|
||||
/// It is a Vulkan driver for AMD GCN/RDNA GPUs.
|
||||
///
|
||||
/// @see <a href="https://docs.mesa3d.org/drivers/radv.html">RADV - The Mesa 3D Graphics Library</a>
|
||||
AMD_RADV("radeon") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return Vulkan.hasCard(cards, HardwareVendor.AMD);
|
||||
}
|
||||
},
|
||||
|
||||
/// Intel Vulkan driver.
|
||||
///
|
||||
/// It is a Vulkan driver for Intel GPUs.
|
||||
INTEL_VULKAN("ig") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return platform.os() == OperatingSystem.WINDOWS && Vulkan.hasCard(cards, HardwareVendor.INTEL);
|
||||
}
|
||||
},
|
||||
|
||||
/// Mesa ANV driver.
|
||||
///
|
||||
/// It is a Vulkan driver for Intel GPUs.
|
||||
///
|
||||
/// @see <a href="https://docs.mesa3d.org/drivers/anv.html">ANV - The Mesa 3D Graphics Library</a>
|
||||
INTEL_ANV("intel") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return platform.os() != OperatingSystem.WINDOWS && Vulkan.hasCard(cards, HardwareVendor.INTEL);
|
||||
}
|
||||
},
|
||||
|
||||
/// Intel HASVK driver.
|
||||
///
|
||||
/// It is a Vulkan driver for Intel Gen7 (Ivy Bridge / Haswell) and Gen8 (Broadwell) graphics.
|
||||
///
|
||||
/// @see <a href="https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/18208">intel: split vulkan driver between gfx7/8 and above</a>
|
||||
INTEL_HASVK("intel_hasvk") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return platform.os() != OperatingSystem.WINDOWS
|
||||
&& cards != null
|
||||
&& cards.stream().anyMatch(card -> card.getVendor() == HardwareVendor.INTEL && card.getName().startsWith("Intel HD Graphics "));
|
||||
}
|
||||
},
|
||||
|
||||
/// Qualcomm Vulkan driver.
|
||||
///
|
||||
/// It is a Vulkan driver for Qualcomm Adreno GPUs.
|
||||
QUALCOMM("qc") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return platform.equals(Platform.WINDOWS_ARM64);
|
||||
}
|
||||
},
|
||||
|
||||
/// Mesa Turnip driver.
|
||||
///
|
||||
/// It is a Vulkan driver for Qualcomm Adreno GPUs.
|
||||
///
|
||||
/// @see <a href="https://docs.mesa3d.org/drivers/freedreno.html">Freedreno - The Mesa 3D Graphics Library</a>
|
||||
TURNIP("freedreno") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return platform.os() != OperatingSystem.WINDOWS && platform.arch().isArm();
|
||||
}
|
||||
},
|
||||
|
||||
/// MoltenVK driver.
|
||||
///
|
||||
/// It is a Vulkan driver for macOS, iOS, tvOS, and visionOS.
|
||||
///
|
||||
/// @see <a href="https://github.com/KhronosGroup/MoltenVK">KhronosGroup/MoltenVK - GitHub</a>
|
||||
MOLTENVK("MoltenVK") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return platform.os() == OperatingSystem.MACOS;
|
||||
}
|
||||
},
|
||||
|
||||
/// Mesa KosmicKrisp driver.
|
||||
///
|
||||
/// It is a Vulkan driver for Apple Silicon hardware.
|
||||
///
|
||||
/// @see <a href="https://docs.mesa3d.org/drivers/kosmickrisp.html">KosmicKrisp - The Mesa 3D Graphics Library</a>
|
||||
KOSMICKRISP("kosmickrisp_mesa") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return platform.os() == OperatingSystem.MACOS && platform.arch() == Architecture.ARM64;
|
||||
}
|
||||
},
|
||||
|
||||
/// Mesa PowerVR driver.
|
||||
///
|
||||
/// It is a Vulkan driver for Imagination Technologies PowerVR GPUs.
|
||||
///
|
||||
/// @see <a href="https://docs.mesa3d.org/drivers/powervr.html">PowerVR - The Mesa 3D Graphics Library</a>
|
||||
POWERVR("powervr") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return platform.os() == OperatingSystem.LINUX && Vulkan.hasCard(cards, HardwareVendor.IMG);
|
||||
}
|
||||
},
|
||||
|
||||
/// Mesa PanVK driver.
|
||||
///
|
||||
/// It is a Vulkan driver for ARM Mali GPUs.
|
||||
///
|
||||
/// @see <a href="https://docs.mesa3d.org/drivers/panfrost.html">Panfrost - The Mesa 3D Graphics Library</a>
|
||||
PANVK("panfrost") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return platform.os() == OperatingSystem.LINUX && platform.arch().isArm();
|
||||
}
|
||||
},
|
||||
|
||||
/// Mesa V3DV driver.
|
||||
///
|
||||
/// It is a Vulkan driver for the Raspberry Pi 4 and Raspberry Pi 5.
|
||||
///
|
||||
/// @see <a href="https://docs.mesa3d.org/drivers/v3dv.html">V3DV - The Mesa 3D Graphics Library</a>
|
||||
V3DV("broadcom") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return platform.os() == OperatingSystem.LINUX && platform.arch().isArm()
|
||||
&& Vulkan.hasCard(cards, HardwareVendor.BROADCOM);
|
||||
}
|
||||
};
|
||||
|
||||
private static final class Holder {
|
||||
static final List<Renderer> SUPPORTED;
|
||||
static final Map<Vulkan, Path> DRIVER_TO_ICD_FILE;
|
||||
|
||||
static {
|
||||
var driverToIcdFile = new EnumMap<Vulkan, Path>(Vulkan.class);
|
||||
var supported = new LinkedHashSet<Renderer>();
|
||||
|
||||
|
||||
// Helper
|
||||
String archName = switch (Architecture.SYSTEM_ARCH) {
|
||||
case X86 -> "i686";
|
||||
case X86_64 -> "x86_64";
|
||||
case ARM64 -> "aarch64";
|
||||
default -> Architecture.SYSTEM_ARCH.getCheckedName();
|
||||
};
|
||||
|
||||
var icdFileNamePattern = Pattern.compile("(?<name>[a-zA-Z0-9_-]+)_icd(?:\\." + Pattern.quote(archName) + ")?\\.json");
|
||||
Map<String, Vulkan> icdNameToDriver = Stream.of(values()).collect(Collectors.toMap(Vulkan::icdName, Function.identity()));
|
||||
|
||||
supported.add(DEFAULT);
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
supported.addAll(List.of(LAVAPIPE, DOZEN));
|
||||
|
||||
List<GraphicsCard> graphicsCards = SystemInfo.getGraphicsCards();
|
||||
if (graphicsCards != null) {
|
||||
EnumSet<Vulkan> foundSupported = EnumSet.noneOf(Vulkan.class);
|
||||
for (GraphicsCard card : graphicsCards) {
|
||||
if (!card.getVulkanDriverFiles().isEmpty()) {
|
||||
for (Path icdFile : card.getVulkanDriverFiles()) {
|
||||
String fileName = FileUtils.getName(icdFile);
|
||||
if (!fileName.endsWith(".json"))
|
||||
continue;
|
||||
|
||||
Vulkan driver;
|
||||
|
||||
Matcher matcher = icdFileNamePattern.matcher(fileName);
|
||||
if (matcher.matches()) {
|
||||
String icdName = matcher.group("name");
|
||||
driver = icdNameToDriver.get(icdName);
|
||||
} else {
|
||||
switch (fileName.substring(0, fileName.length() - ".json".length())) {
|
||||
case "igvk64" -> {
|
||||
if (Architecture.SYSTEM_ARCH.getBits() == Bits.BIT_64)
|
||||
driver = INTEL_VULKAN;
|
||||
else
|
||||
continue;
|
||||
}
|
||||
case "igvk32" -> {
|
||||
if (Architecture.SYSTEM_ARCH.getBits() == Bits.BIT_32)
|
||||
driver = INTEL_VULKAN;
|
||||
else
|
||||
continue;
|
||||
}
|
||||
case "nv-vk64" -> {
|
||||
if (Architecture.SYSTEM_ARCH.getBits() == Bits.BIT_64)
|
||||
driver = NVIDIA_VULKAN;
|
||||
else
|
||||
continue;
|
||||
}
|
||||
case "nv-vk32" -> {
|
||||
if (Architecture.SYSTEM_ARCH.getBits() == Bits.BIT_32)
|
||||
driver = NVIDIA_VULKAN;
|
||||
else
|
||||
continue;
|
||||
}
|
||||
case "amd-vulkan64" -> {
|
||||
if (Architecture.SYSTEM_ARCH.getBits() == Bits.BIT_64)
|
||||
driver = AMDVLK;
|
||||
else
|
||||
continue;
|
||||
}
|
||||
case "amd-vulkan32" -> {
|
||||
if (Architecture.SYSTEM_ARCH.getBits() == Bits.BIT_32)
|
||||
driver = AMDVLK;
|
||||
else
|
||||
continue;
|
||||
}
|
||||
case "qcvk_icd_arm64x" -> {
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
|
||||
driver = QUALCOMM;
|
||||
else
|
||||
continue;
|
||||
}
|
||||
default -> {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
driverToIcdFile.putIfAbsent(driver, icdFile);
|
||||
foundSupported.add(driver);
|
||||
}
|
||||
}
|
||||
}
|
||||
supported.addAll(foundSupported);
|
||||
}
|
||||
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
// LWJGL integrates MoltenVK, so it is always available
|
||||
supported.add(MOLTENVK);
|
||||
|
||||
// We need libvulkan.1.dylib to load custom Vulkan drivers
|
||||
if (Files.isRegularFile(HomebrewUtils.LIB_VULKAN)) {
|
||||
Path kosmickrispIcd = HomebrewUtils.HOMEBREW_PREFIX.resolve("share/vulkan/icd.d/kosmickrisp_mesa_icd." + archName + ".json");
|
||||
if (Files.isRegularFile(kosmickrispIcd)) {
|
||||
driverToIcdFile.put(KOSMICKRISP, kosmickrispIcd);
|
||||
supported.add(KOSMICKRISP);
|
||||
}
|
||||
|
||||
Path lvpIcd = HomebrewUtils.HOMEBREW_PREFIX.resolve("share/vulkan/icd.d/lvp_icd." + archName + ".json");
|
||||
if (Files.isRegularFile(lvpIcd)) {
|
||||
driverToIcdFile.put(LAVAPIPE, lvpIcd);
|
||||
supported.add(LAVAPIPE);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
List<Path> icdDirs = switch (OperatingSystem.CURRENT_OS) {
|
||||
case LINUX -> List.of(
|
||||
Path.of("/usr/share/vulkan/icd.d"),
|
||||
Path.of("/etc/vulkan/icd.d")
|
||||
);
|
||||
case FREEBSD -> List.of(Path.of("/usr/local/share/vulkan/icd.d"));
|
||||
default -> List.of();
|
||||
};
|
||||
|
||||
EnumSet<Vulkan> foundSupported = EnumSet.noneOf(Vulkan.class);
|
||||
for (Path icdDir : icdDirs) {
|
||||
if (!Files.isDirectory(icdDir))
|
||||
continue;
|
||||
try (Stream<Path> stream = Files.list(icdDir)) {
|
||||
for (Path icdFile : Lang.toIterable(stream)) {
|
||||
String fileName = icdFile.getFileName().toString();
|
||||
|
||||
Matcher matcher = icdFileNamePattern.matcher(fileName);
|
||||
if (matcher.matches()) {
|
||||
String icdName = matcher.group("name");
|
||||
|
||||
Vulkan driver = icdNameToDriver.get(icdName);
|
||||
if (driver != null) {
|
||||
driverToIcdFile.put(driver, icdFile);
|
||||
|
||||
if (driver.isSupported(Platform.CURRENT_PLATFORM, SystemInfo.getGraphicsCards())) {
|
||||
foundSupported.add(driver);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to read Vulkan ICD files in " + icdDir, e);
|
||||
}
|
||||
}
|
||||
|
||||
supported.addAll(foundSupported);
|
||||
}
|
||||
|
||||
SUPPORTED = List.copyOf(supported);
|
||||
DRIVER_TO_ICD_FILE = Collections.unmodifiableMap(driverToIcdFile);
|
||||
}
|
||||
}
|
||||
|
||||
private final String icdName;
|
||||
|
||||
Vulkan(String icdName) {
|
||||
this.icdName = icdName;
|
||||
}
|
||||
|
||||
private static boolean hasCard(@Nullable List<GraphicsCard> cards, HardwareVendor vendor) {
|
||||
return cards != null && cards.stream().anyMatch(card -> card.getVendor() == vendor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphicsAPI api() {
|
||||
return GraphicsAPI.VULKAN;
|
||||
}
|
||||
|
||||
@Contract(pure = true)
|
||||
public String icdName() {
|
||||
return icdName;
|
||||
}
|
||||
|
||||
/// Get the path to the ICD file for this driver.
|
||||
///
|
||||
/// If the ICD file does not exist, return `null`.
|
||||
@Contract(pure = true)
|
||||
public @Nullable Path icdFile() {
|
||||
return Holder.DRIVER_TO_ICD_FILE.get(this);
|
||||
}
|
||||
}
|
||||
|
||||
enum OpenGL implements Driver {
|
||||
/// @see <a href="https://docs.mesa3d.org/drivers/llvmpipe.html">LLVMpipe - The Mesa 3D Graphics Library</a>
|
||||
LLVMPIPE {
|
||||
@Override
|
||||
public String mesaDriverName() {
|
||||
return "llvmpipe";
|
||||
}
|
||||
},
|
||||
ZINK {
|
||||
@Override
|
||||
public String mesaDriverName() {
|
||||
return "zink";
|
||||
}
|
||||
},
|
||||
D3D12 {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return platform.os() == OperatingSystem.WINDOWS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String mesaDriverName() {
|
||||
return "d3d12";
|
||||
}
|
||||
};
|
||||
|
||||
static final List<Renderer> SUPPORTED =
|
||||
Stream.concat(
|
||||
Stream.of(DEFAULT),
|
||||
Stream.of(OpenGL.values()).filter(it -> it.isSupported(
|
||||
Platform.CURRENT_PLATFORM,
|
||||
null // We don't need to pass graphics cards for OpenGL yet
|
||||
))
|
||||
).toList();
|
||||
|
||||
@Override
|
||||
public GraphicsAPI api() {
|
||||
return GraphicsAPI.OPENGL;
|
||||
}
|
||||
}
|
||||
|
||||
/// Unknown renderer.
|
||||
record Unknown(String name) implements Renderer {
|
||||
}
|
||||
|
||||
/// Whether this renderer is supported on the given platform and graphics cards.
|
||||
///
|
||||
/// If `cards` is `null`, it means that the graphics cards are unknown.
|
||||
default boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -88,13 +88,15 @@ public final class World {
|
||||
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");
|
||||
}
|
||||
try (Stream<Path> filesStream = Files.list(fs.getPath("/"))) {
|
||||
List<Path> files = filesStream.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);
|
||||
root = files.get(0);
|
||||
fileName = FileUtils.getName(root);
|
||||
}
|
||||
}
|
||||
|
||||
Path levelDat = root.resolve("level.dat");
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.Unzipper;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
import org.jackhuang.hmcl.util.platform.macos.HomebrewUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -104,11 +105,11 @@ public class DefaultLauncher extends Launcher {
|
||||
|
||||
// Executable
|
||||
if (StringUtils.isNotBlank(options.getWrapper()))
|
||||
res.addAllWithoutParsing(StringUtils.tokenize(options.getWrapper(), getEnvVars()));
|
||||
res.addAllWithoutParsing(StringUtils.tokenize(options.getWrapper(), getEnvVars(nativeFolder)));
|
||||
|
||||
res.add(options.getJava().getBinary().toString());
|
||||
|
||||
res.addAllWithoutParsing(options.getOverrideJavaArguments());
|
||||
res.addAllWithoutParsingAndReadExternal(options.getOverrideJavaArguments());
|
||||
|
||||
if (options.getMaxMemory() != null && options.getMaxMemory() > 0)
|
||||
res.addDefault("-Xmx", options.getMaxMemory() + "m");
|
||||
@@ -265,6 +266,21 @@ public class DefaultLauncher extends Launcher {
|
||||
res.addDefault("-Dfml.ignorePatchDiscrepancies=", "true");
|
||||
}
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
|
||||
&& options.getRenderer() instanceof Renderer.Driver renderer
|
||||
&& renderer.mesaDriverName() != null) {
|
||||
res.addDefault("-Dorg.glavo.mesa.loader.nativeDir=", FileUtils.getAbsolutePath(nativeFolder.resolve("mesa-loader")));
|
||||
}
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
|
||||
&& options.getJava().getArchitecture() == Architecture.SYSTEM_ARCH
|
||||
&& options.getRenderer() instanceof Renderer.Vulkan vulkanDriver
|
||||
&& vulkanDriver.icdFile() != null) {
|
||||
if (Files.isRegularFile(HomebrewUtils.LIB_VULKAN)) {
|
||||
res.addDefault("-Dorg.lwjgl.vulkan.libname=", FileUtils.getAbsolutePath(HomebrewUtils.LIB_VULKAN));
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> classpath = repository.getClasspath(version);
|
||||
|
||||
if (analyzer.has(LibraryAnalyzer.LibraryType.CLEANROOM)) {
|
||||
@@ -395,6 +411,12 @@ public class DefaultLauncher extends Launcher {
|
||||
}
|
||||
}
|
||||
|
||||
if (options.getGraphicsBackend() != GraphicsAPI.DEFAULT
|
||||
&& gameVersion.isPresent() && GameVersionNumber.compare(gameVersion.get(), "26.2-snapshot-2") >= 0) {
|
||||
res.add("--graphicsBackend");
|
||||
res.add(options.getGraphicsBackend().getMinecraftArg());
|
||||
}
|
||||
|
||||
res.addAllWithoutParsing(Arguments.parseStringArguments(options.getGameArguments(), configuration));
|
||||
|
||||
res.removeIf(it -> getForbiddens().containsKey(it) && getForbiddens().get(it).get());
|
||||
@@ -564,8 +586,8 @@ public class DefaultLauncher extends Launcher {
|
||||
Path runDirectory = repository.getRunDirectory(version.getId());
|
||||
|
||||
if (StringUtils.isNotBlank(options.getPreLaunchCommand())) {
|
||||
ProcessBuilder builder = new ProcessBuilder(StringUtils.tokenize(options.getPreLaunchCommand(), getEnvVars())).directory(runDirectory.toFile());
|
||||
builder.environment().putAll(getEnvVars());
|
||||
ProcessBuilder builder = new ProcessBuilder(StringUtils.tokenize(options.getPreLaunchCommand(), getEnvVars(nativeFolder))).directory(runDirectory.toFile());
|
||||
builder.environment().putAll(getEnvVars(nativeFolder));
|
||||
SystemUtils.callExternalProcess(builder);
|
||||
}
|
||||
|
||||
@@ -578,7 +600,7 @@ public class DefaultLauncher extends Launcher {
|
||||
Path appdata = options.getGameDir().toAbsolutePath().getParent();
|
||||
if (appdata != null) builder.environment().put("APPDATA", appdata.toString());
|
||||
|
||||
builder.environment().putAll(getEnvVars());
|
||||
builder.environment().putAll(getEnvVars(nativeFolder));
|
||||
process = builder.start();
|
||||
} catch (IOException e) {
|
||||
throw new ProcessCreationException(e);
|
||||
@@ -586,12 +608,13 @@ public class DefaultLauncher extends Launcher {
|
||||
|
||||
ManagedProcess p = new ManagedProcess(process, rawCommandLine);
|
||||
if (listener != null)
|
||||
startMonitors(p, listener, command.encoding, daemon);
|
||||
startMonitors(p, nativeFolder, listener, command.encoding, daemon);
|
||||
return p;
|
||||
}
|
||||
|
||||
private Map<String, String> getEnvVars() {
|
||||
private Map<String, String> getEnvVars(Path nativeFolder) {
|
||||
String versionName = Optional.ofNullable(options.getVersionName()).orElse(version.getId());
|
||||
|
||||
Map<String, String> env = new LinkedHashMap<>();
|
||||
env.put("INST_NAME", versionName);
|
||||
env.put("INST_ID", versionName);
|
||||
@@ -599,18 +622,32 @@ public class DefaultLauncher extends Launcher {
|
||||
env.put("INST_MC_DIR", FileUtils.getAbsolutePath(repository.getRunDirectory(version.getId())));
|
||||
env.put("INST_JAVA", options.getJava().getBinary().toString());
|
||||
|
||||
Renderer renderer = options.getRenderer();
|
||||
if (renderer != Renderer.DEFAULT) {
|
||||
if (options.getRenderer() instanceof Renderer.Driver driver) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
if (renderer != Renderer.LLVMPIPE)
|
||||
env.put("GALLIUM_DRIVER", renderer.name().toLowerCase(Locale.ROOT));
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
|
||||
env.put("__GLX_VENDOR_LIBRARY_NAME", "mesa");
|
||||
switch (renderer) {
|
||||
case LLVMPIPE:
|
||||
if (driver.mesaDriverName() != null) {
|
||||
if (driver instanceof Renderer.OpenGL && driver != Renderer.OpenGL.LLVMPIPE)
|
||||
env.put("GALLIUM_DRIVER", driver.mesaDriverName());
|
||||
else if (driver instanceof Renderer.Vulkan vulkanDriver) {
|
||||
String icdFile = FileUtils.getAbsolutePath(nativeFolder.resolve("mesa-loader/" + vulkanDriver.icdName() + "_icd.json"));
|
||||
|
||||
env.put("VK_ICD_FILENAMES", icdFile);
|
||||
env.put("VK_DRIVER_FILES", icdFile);
|
||||
}
|
||||
} else if (driver instanceof Renderer.Vulkan vulkanDriver
|
||||
&& vulkanDriver.icdFile() != null
|
||||
&& options.getJava().getArchitecture() == Architecture.SYSTEM_ARCH) {
|
||||
String icdFile = FileUtils.getAbsolutePath(vulkanDriver.icdFile());
|
||||
|
||||
env.put("VK_ICD_FILENAMES", icdFile);
|
||||
env.put("VK_DRIVER_FILES", icdFile);
|
||||
}
|
||||
} else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
|
||||
if (driver instanceof Renderer.OpenGL oglDriver) {
|
||||
if (oglDriver == Renderer.OpenGL.LLVMPIPE) {
|
||||
env.put("__GLX_VENDOR_LIBRARY_NAME", "mesa");
|
||||
env.put("LIBGL_ALWAYS_SOFTWARE", "1");
|
||||
break;
|
||||
case ZINK:
|
||||
} else if (oglDriver == Renderer.OpenGL.ZINK) {
|
||||
env.put("__GLX_VENDOR_LIBRARY_NAME", "mesa");
|
||||
env.put("MESA_LOADER_DRIVER_OVERRIDE", "zink");
|
||||
/*
|
||||
* The amdgpu DDX is missing support for modifiers, causing Zink to fail.
|
||||
@@ -619,7 +656,23 @@ public class DefaultLauncher extends Launcher {
|
||||
* Link: https://gitlab.freedesktop.org/mesa/mesa/-/issues/10093
|
||||
*/
|
||||
env.put("LIBGL_KOPPER_DRI2", "1");
|
||||
break;
|
||||
}
|
||||
} else if (driver instanceof Renderer.Vulkan vulkanDriver
|
||||
&& options.getJava().getArchitecture() == Architecture.SYSTEM_ARCH) {
|
||||
if (vulkanDriver.icdFile() != null) {
|
||||
String absolutePath = FileUtils.getAbsolutePath(vulkanDriver.icdFile());
|
||||
env.put("VK_ICD_FILENAMES", absolutePath);
|
||||
env.put("VK_DRIVER_FILES", absolutePath);
|
||||
}
|
||||
}
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
|
||||
&& options.getJava().getArchitecture() == Architecture.SYSTEM_ARCH) {
|
||||
if (driver instanceof Renderer.Vulkan vulkanDriver
|
||||
&& vulkanDriver != Renderer.Vulkan.MOLTENVK
|
||||
&& vulkanDriver.icdFile() != null) {
|
||||
String absolutePath = FileUtils.getAbsolutePath(vulkanDriver.icdFile());
|
||||
env.put("VK_ICD_FILENAMES", absolutePath);
|
||||
env.put("VK_DRIVER_FILES", absolutePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -669,15 +722,15 @@ public class DefaultLauncher extends Launcher {
|
||||
boolean usePowerShell = "ps1".equals(scriptExtension);
|
||||
|
||||
if (!usePowerShell) {
|
||||
if (isWindows && !scriptExtension.equals("bat"))
|
||||
if (isWindows && !scriptExtension.equalsIgnoreCase("bat"))
|
||||
throw new IllegalArgumentException("The extension of " + scriptFile + " is not 'bat' or 'ps1' in Windows");
|
||||
else if (!isWindows && !(scriptExtension.equals("sh") || scriptExtension.equals("command")))
|
||||
throw new IllegalArgumentException("The extension of " + scriptFile + " is not 'sh', 'ps1' or 'command' in macOS/Linux");
|
||||
else if (!isWindows && !(scriptExtension.equalsIgnoreCase("sh") || scriptExtension.equalsIgnoreCase("command") || scriptExtension.equalsIgnoreCase("bash")))
|
||||
throw new IllegalArgumentException("The extension of " + scriptFile + " is not 'sh', 'bash', 'ps1' or 'command' in macOS/Linux");
|
||||
}
|
||||
|
||||
final Command commandLine = generateCommandLine(nativeFolder);
|
||||
final String command = usePowerShell ? null : commandLine.commandLine.toString();
|
||||
Map<String, String> envVars = getEnvVars();
|
||||
Map<String, String> envVars = getEnvVars(nativeFolder);
|
||||
|
||||
if (isWindows && !usePowerShell) {
|
||||
// https://stackoverflow.com/a/28452546
|
||||
@@ -820,7 +873,7 @@ public class DefaultLauncher extends Launcher {
|
||||
throw new ExecutionPolicyLimitException();
|
||||
}
|
||||
|
||||
private void startMonitors(ManagedProcess managedProcess, ProcessListener processListener, Charset encoding, boolean isDaemon) {
|
||||
private void startMonitors(ManagedProcess managedProcess, Path nativeFolder, ProcessListener processListener, Charset encoding, boolean isDaemon) {
|
||||
processListener.setProcess(managedProcess);
|
||||
Thread stdout = Lang.thread(new StreamPump(managedProcess.getProcess().getInputStream(), it -> {
|
||||
processListener.onLog(it, false);
|
||||
@@ -837,8 +890,8 @@ public class DefaultLauncher extends Launcher {
|
||||
|
||||
if (StringUtils.isNotBlank(options.getPostExitCommand())) {
|
||||
try {
|
||||
ProcessBuilder builder = new ProcessBuilder(StringUtils.tokenize(options.getPostExitCommand(), getEnvVars())).directory(options.getGameDir().toFile());
|
||||
builder.environment().putAll(getEnvVars());
|
||||
ProcessBuilder builder = new ProcessBuilder(StringUtils.tokenize(options.getPostExitCommand(), getEnvVars(nativeFolder))).directory(options.getGameDir().toFile());
|
||||
builder.environment().putAll(getEnvVars(nativeFolder));
|
||||
SystemUtils.callExternalProcess(builder);
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("An Exception happened while running exit command.", e);
|
||||
|
||||
@@ -365,14 +365,16 @@ public final class ModManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if "mods" directory has mod file named "fileName" no matter the mod is disabled or not
|
||||
* Check if "mods" directory has mod file named "fileName" no matter the mod is disabled,upgraded or not
|
||||
*
|
||||
* @param fileName name of the file whose existence is being checked
|
||||
* @return true if the file exists
|
||||
*/
|
||||
public boolean hasSimpleMod(String fileName) {
|
||||
return Files.exists(getModsDirectory().resolve(StringUtils.removeSuffix(fileName, DISABLED_EXTENSION)))
|
||||
|| Files.exists(getModsDirectory().resolve(StringUtils.addSuffix(fileName, DISABLED_EXTENSION)));
|
||||
|| Files.exists(getModsDirectory().resolve(StringUtils.addSuffix(fileName, DISABLED_EXTENSION)))
|
||||
|| Files.exists(getModsDirectory().resolve(StringUtils.removeSuffix(fileName, OLD_EXTENSION)))
|
||||
|| Files.exists(getModsDirectory().resolve(StringUtils.addSuffix(fileName, OLD_EXTENSION)));
|
||||
}
|
||||
|
||||
public Path getSimpleModPath(String fileName) {
|
||||
|
||||
@@ -24,6 +24,8 @@ import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -211,7 +213,13 @@ public class CurseAddon implements RemoteMod.IMod {
|
||||
}
|
||||
|
||||
public RemoteMod toMod() {
|
||||
String iconUrl = Optional.ofNullable(logo).map(Logo::getThumbnailUrl).orElse("");
|
||||
String iconUrl = "";
|
||||
if (logo != null) {
|
||||
if (StringUtils.isNotBlank(logo.getThumbnailUrl()))
|
||||
iconUrl = logo.getThumbnailUrl();
|
||||
else if (StringUtils.isNotBlank(logo.getUrl()))
|
||||
iconUrl = logo.getUrl();
|
||||
}
|
||||
|
||||
return new RemoteMod(
|
||||
slug,
|
||||
@@ -585,7 +593,7 @@ public class CurseAddon implements RemoteMod.IMod {
|
||||
}
|
||||
return RemoteMod.Dependency.ofGeneral(RELATION_TYPE.get(dependency.getRelationType()), CurseForgeRemoteModRepository.MODS, Integer.toString(dependency.getModId()));
|
||||
}).distinct().filter(Objects::nonNull).collect(Collectors.toList()),
|
||||
gameVersions.stream().filter(ver -> ver.startsWith("1.") || ver.contains("w")).collect(Collectors.toList()),
|
||||
gameVersions.stream().filter(GameVersionNumber::isKnown).toList(),
|
||||
gameVersions.stream().flatMap(version -> {
|
||||
if ("fabric".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.FABRIC);
|
||||
else if ("forge".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.FORGE);
|
||||
|
||||
@@ -188,9 +188,13 @@ public final class CurseCompletionTask extends Task<Void> {
|
||||
String fileName = file.fileName();
|
||||
return switch (classID) {
|
||||
case 12, // Resource pack
|
||||
6552 -> { // Shader pack
|
||||
Path res = (classID == 12 ? resourcePacksRoot : shaderPacksRoot).resolve(fileName);
|
||||
yield Files.exists(res) ? null : res;
|
||||
6945 -> { // Data pack
|
||||
Path path = resourcePacksRoot.resolve(fileName);
|
||||
yield Files.exists(path) ? null : path;
|
||||
}
|
||||
case 6552 -> { // Shader pack
|
||||
Path path = shaderPacksRoot.resolve(fileName);
|
||||
yield Files.exists(path) ? null : path;
|
||||
}
|
||||
default -> {
|
||||
if (modManager.hasSimpleMod(fileName)) {
|
||||
|
||||
@@ -24,7 +24,6 @@ import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import com.moandjiezana.toml.Toml;
|
||||
import kala.compress.archivers.zip.ZipArchiveEntry;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
@@ -35,6 +34,10 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.tree.ZipFileTree;
|
||||
import org.tomlj.Toml;
|
||||
import org.tomlj.TomlArray;
|
||||
import org.tomlj.TomlParseResult;
|
||||
import org.tomlj.TomlTable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -42,8 +45,8 @@ import java.lang.reflect.Type;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.Manifest;
|
||||
@@ -208,10 +211,15 @@ public final class ForgeNewModMetadata {
|
||||
ZipArchiveEntry modToml = tree.getEntry(tomlPath);
|
||||
if (modToml == null)
|
||||
throw new IOException("File " + modFile + " is not a Forge 1.13+ or NeoForge mod.");
|
||||
Toml toml = new Toml().read(tree.readTextEntry(modToml));
|
||||
ForgeNewModMetadata metadata = toml.to(ForgeNewModMetadata.class);
|
||||
TomlParseResult tomlParseResult = Toml.parse(tree.readTextEntry(modToml));
|
||||
if (tomlParseResult.hasErrors()) {
|
||||
var ioException = new IOException("Mod " + modFile + " `%s` is malformed..".formatted(modToml.getName()));
|
||||
tomlParseResult.errors().forEach(ioException::addSuppressed);
|
||||
throw ioException;
|
||||
}
|
||||
ForgeNewModMetadata metadata = JsonUtils.GSON.fromJson(tomlParseResult.toJson(), ForgeNewModMetadata.class);
|
||||
if (metadata == null || metadata.getMods().isEmpty())
|
||||
throw new IOException("Mod " + modFile + " `mods.toml` is malformed..");
|
||||
throw new IOException("Mod " + modFile + " `%s` is malformed..".formatted(modToml.getName()));
|
||||
Mod mod = metadata.getMods().get(0);
|
||||
ZipArchiveEntry manifestMF = tree.getEntry("META-INF/MANIFEST.MF");
|
||||
String jarVersion = "";
|
||||
@@ -224,7 +232,7 @@ public final class ForgeNewModMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
ModLoaderType type = analyzeLoader(toml, mod.getModId(), modLoaderType);
|
||||
ModLoaderType type = analyzeLoader(tomlParseResult, mod.getModId(), modLoaderType);
|
||||
|
||||
return new LocalModFile(modManager, modManager.getLocalMod(mod.getModId(), type), modFile, mod.getDisplayName(), new LocalModFile.Description(mod.getDescription()),
|
||||
mod.getAuthors(), jarVersion == null ? mod.getVersion() : mod.getVersion().replace("${file.jarVersion}", jarVersion), "",
|
||||
@@ -293,23 +301,32 @@ public final class ForgeNewModMetadata {
|
||||
throw new IOException();
|
||||
}
|
||||
|
||||
private static ModLoaderType analyzeLoader(Toml toml, String modID, ModLoaderType loader) {
|
||||
List<HashMap<String, Object>> dependencies = null;
|
||||
private static ModLoaderType analyzeLoader(TomlParseResult toml, String modID, ModLoaderType loader) {
|
||||
List<Map<String, Object>> dependencies = null;
|
||||
try {
|
||||
dependencies = toml.getList("dependencies." + modID);
|
||||
TomlArray tomlArray = toml.getArray("dependencies." + modID);
|
||||
if (tomlArray != null) {
|
||||
dependencies = tomlArray.toList().stream().map( o -> ((TomlTable) o).toMap()).toList();
|
||||
}
|
||||
} catch (ClassCastException ignored) { // https://github.com/HMCL-dev/HMCL/issues/5068
|
||||
}
|
||||
|
||||
if (dependencies == null) {
|
||||
try {
|
||||
dependencies = toml.getList("dependencies"); // ??? I have no idea why some of the Forge mods use [[dependencies]]
|
||||
TomlArray tomlArray = toml.getArray("dependencies"); // ??? I have no idea why some of the Forge mods use [[dependencies]]
|
||||
if (tomlArray != null) {
|
||||
dependencies = tomlArray.toList().stream().map( o -> ((TomlTable) o).toMap()).toList();
|
||||
}
|
||||
} catch (ClassCastException e) {
|
||||
try {
|
||||
Toml table = toml.getTable("dependencies");
|
||||
TomlTable table = toml.getTable("dependencies");
|
||||
if (table == null)
|
||||
return loader;
|
||||
|
||||
dependencies = table.getList(modID);
|
||||
TomlArray tomlArray = table.getArray(modID);
|
||||
if (tomlArray != null) {
|
||||
dependencies = tomlArray.toList().stream().map( o -> ((TomlTable) o).toMap()).toList();
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
@@ -321,7 +338,7 @@ public final class ForgeNewModMetadata {
|
||||
|
||||
ModLoaderType result = null;
|
||||
loop:
|
||||
for (HashMap<String, Object> dependency : dependencies) {
|
||||
for (Map<String, Object> dependency : dependencies) {
|
||||
switch ((String) dependency.get("modId")) {
|
||||
case "forge":
|
||||
result = ModLoaderType.FORGE;
|
||||
|
||||
+44
-6
@@ -24,11 +24,7 @@ import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
import org.jackhuang.hmcl.util.DigestUtils;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
@@ -42,6 +38,7 @@ import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -63,6 +60,41 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
||||
public static final ModrinthRemoteModRepository RESOURCE_PACKS = new ModrinthRemoteModRepository("resourcepack");
|
||||
public static final ModrinthRemoteModRepository SHADER_PACKS = new ModrinthRemoteModRepository("shader");
|
||||
|
||||
private static final Comparator<String> TAG_COMPARATOR = PriorityComparator.of(
|
||||
List.of("babric",
|
||||
"bta-babric",
|
||||
"bukkit",
|
||||
"bungeecord",
|
||||
"canvas",
|
||||
"datapack",
|
||||
"fabric",
|
||||
"folia",
|
||||
"forge",
|
||||
"geyser",
|
||||
"iris",
|
||||
"java-agent",
|
||||
"legacy-fabric",
|
||||
"liteloader",
|
||||
"minecraft",
|
||||
"modloader",
|
||||
"mrpack",
|
||||
"neoforge",
|
||||
"nilloader",
|
||||
"optifine",
|
||||
"ornith",
|
||||
"paper",
|
||||
"purpur",
|
||||
"quilt",
|
||||
"rift",
|
||||
"spigot",
|
||||
"sponge",
|
||||
"vanilla",
|
||||
"velocity",
|
||||
"waterfall"),
|
||||
Comparator.naturalOrder(),
|
||||
false
|
||||
);
|
||||
|
||||
private static final Semaphore SEMAPHORE = new Semaphore(16);
|
||||
|
||||
private static final String PREFIX = "https://api.modrinth.com";
|
||||
@@ -95,6 +127,12 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
||||
}
|
||||
}
|
||||
|
||||
static List<String> sortDisplayCategories(List<String> displayCategories) {
|
||||
return displayCategories != null && !displayCategories.isEmpty()
|
||||
? displayCategories.stream().sorted(TAG_COMPARATOR).toList()
|
||||
: List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
||||
SEMAPHORE.acquireUninterruptibly();
|
||||
@@ -811,7 +849,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
||||
author,
|
||||
title,
|
||||
description,
|
||||
displayCategories,
|
||||
sortDisplayCategories(displayCategories),
|
||||
String.format("https://modrinth.com/%s/%s", projectType, projectId),
|
||||
iconUrl,
|
||||
this
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 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.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/// A comparator that orders elements based on a predefined priority map,
|
||||
/// falling back to another comparator for elements not present in the map.
|
||||
///
|
||||
/// Elements found in the priority map are compared by their associated priority values.
|
||||
/// When only one of the two elements has a priority, the `prioritizedFirst` flag
|
||||
/// determines whether the prioritized element is ordered before or after the other.
|
||||
///
|
||||
/// @param <T> the type of elements to be compared
|
||||
public final class PriorityComparator<T> implements Comparator<T> {
|
||||
|
||||
/// Creates a new `PriorityComparator` with a priority map and a fallback comparator.
|
||||
/// Elements with a defined priority are ordered before those without.
|
||||
public static <T extends Comparable<T>> PriorityComparator<T> of(T... values) {
|
||||
Map<T, Integer> priorities = new HashMap<>();
|
||||
for (int i = 0; i < values.length; i++)
|
||||
priorities.put(values[i], i);
|
||||
return new PriorityComparator<>(priorities, Comparator.naturalOrder(), true);
|
||||
}
|
||||
|
||||
/// Creates a new `PriorityComparator` with a priority map, a fallback comparator, and a flag to determine the order of prioritized elements.
|
||||
/// Elements with a defined priority are ordered before those without if `prioritizedFirst` is `true`, otherwise they are ordered after.
|
||||
public static <T> PriorityComparator<T> of(List<T> values, Comparator<? super T> fallback, boolean prioritizedFirst) {
|
||||
Objects.requireNonNull(fallback);
|
||||
|
||||
Map<T, Integer> priorities = new HashMap<>();
|
||||
for (int i = 0; i < values.size(); i++)
|
||||
priorities.put(values.get(i), i);
|
||||
return new PriorityComparator<>(priorities, fallback, prioritizedFirst);
|
||||
}
|
||||
|
||||
/// A mapping from values to their priority. Lower values indicate higher priority.
|
||||
private final @NotNull Map<T, Integer> priorities;
|
||||
|
||||
/// The fallback comparator used when neither element has a defined priority.
|
||||
private final @NotNull Comparator<? super T> fallback;
|
||||
|
||||
/// If `true`, elements with a defined priority are ordered before those without;
|
||||
/// if `false`, they are ordered after.
|
||||
private final boolean prioritizedFirst;
|
||||
|
||||
/// Creates a new `PriorityComparator`.
|
||||
///
|
||||
/// @param priorities a map from values to their priority (lower value = higher priority)
|
||||
/// @param fallback the comparator to use when neither element has a defined priority
|
||||
/// @param prioritizedFirst if `true`, prioritized elements come before non-prioritized ones
|
||||
private PriorityComparator(
|
||||
Map<T, Integer> priorities,
|
||||
Comparator<? super T> fallback,
|
||||
boolean prioritizedFirst) {
|
||||
this.priorities = priorities;
|
||||
this.fallback = fallback;
|
||||
this.prioritizedFirst = prioritizedFirst;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(T value1, T value2) {
|
||||
Integer p1 = priorities.get(value1);
|
||||
Integer p2 = priorities.get(value2);
|
||||
|
||||
if (p1 != null) {
|
||||
if (p2 != null) {
|
||||
return p1.compareTo(p2);
|
||||
}
|
||||
|
||||
return prioritizedFirst ? -1 : 1;
|
||||
} else {
|
||||
if (p2 != null) {
|
||||
return prioritizedFirst ? 1 : -1;
|
||||
} else {
|
||||
return fallback.compare(value1, value2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -209,6 +209,17 @@ public final class FileUtils {
|
||||
return true;
|
||||
}
|
||||
|
||||
/// @see #isNameValidForJar(OperatingSystem, String)
|
||||
public static boolean isNameValidForJar(String name) {
|
||||
return isNameValidForJar(OperatingSystem.CURRENT_OS, name);
|
||||
}
|
||||
|
||||
/// Returns true if the given name is a valid jar file name on the given operating system,
|
||||
/// and `false` otherwise.
|
||||
public static boolean isNameValidForJar(OperatingSystem os, String name) {
|
||||
return !name.contains("!") && isNameValid(os, name);
|
||||
}
|
||||
|
||||
/// Safely get the file size. Returns `0` if the file does not exist or the size cannot be obtained.
|
||||
public static long size(Path file) {
|
||||
try {
|
||||
|
||||
@@ -86,6 +86,10 @@ public enum Architecture {
|
||||
return this == X86 || this == X86_64;
|
||||
}
|
||||
|
||||
public boolean isArm() {
|
||||
return this == ARM32 || this == ARM64;
|
||||
}
|
||||
|
||||
public static final Architecture CURRENT_ARCH;
|
||||
public static final Architecture SYSTEM_ARCH;
|
||||
|
||||
|
||||
@@ -17,11 +17,14 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.platform;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@@ -32,6 +35,12 @@ public final class CommandBuilder {
|
||||
private final OperatingSystem os;
|
||||
private final List<Item> raw = new ArrayList<>();
|
||||
|
||||
/// Java 9+ supports passing JVM options stored in external files using the `@<options-file>` option.
|
||||
///
|
||||
/// This list stores options loaded from those external option files when they are encountered,
|
||||
/// so they can be treated as existing arguments and not overwritten by methods like addDefault.
|
||||
private final List<String> external = new ArrayList<>();
|
||||
|
||||
public CommandBuilder() {
|
||||
this(OperatingSystem.CURRENT_OS);
|
||||
}
|
||||
@@ -48,6 +57,10 @@ public final class CommandBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
private Stream<String> allExistingArgs() {
|
||||
return Stream.concat(raw.stream().map(i -> i.arg), external.stream());
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsing will ignore your manual escaping
|
||||
*
|
||||
@@ -77,9 +90,8 @@ public final class CommandBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandBuilder addWithoutParsing(String... args) {
|
||||
for (String s : args)
|
||||
raw.add(new Item(s, false));
|
||||
public CommandBuilder addWithoutParsing(String arg) {
|
||||
raw.add(new Item(arg, false));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -89,6 +101,26 @@ public final class CommandBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandBuilder addAllWithoutParsingAndReadExternal(Collection<String> args) {
|
||||
for (String s : args) {
|
||||
raw.add(new Item(s, false));
|
||||
|
||||
if (s.startsWith("@")) {
|
||||
try {
|
||||
Path file = Path.of(s.substring(1));
|
||||
if (Files.isRegularFile(file)) {
|
||||
try (Stream<String> lines = Files.lines(file)) {
|
||||
external.addAll(parseArgumentFile(lines));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to read external JVM options file: " + s.substring(1), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public void addAllDefault(Collection<String> args) {
|
||||
addAllDefault(args, true);
|
||||
}
|
||||
@@ -98,7 +130,6 @@ public final class CommandBuilder {
|
||||
}
|
||||
|
||||
private void addAllDefault(Collection<String> args, boolean parse) {
|
||||
loop:
|
||||
for (String arg : args) {
|
||||
if (arg.startsWith("-D")) {
|
||||
int idx = arg.indexOf('=');
|
||||
@@ -106,13 +137,16 @@ public final class CommandBuilder {
|
||||
addDefault(arg.substring(0, idx + 1), arg.substring(idx + 1), parse);
|
||||
} else {
|
||||
String opt = arg + "=";
|
||||
for (Item item : raw) {
|
||||
if (item.arg.startsWith(opt)) {
|
||||
LOG.info("Default option '" + arg + "' is suppressed by '" + item.arg + "'");
|
||||
continue loop;
|
||||
} else if (item.arg.equals(arg)) {
|
||||
continue loop;
|
||||
|
||||
Optional<String> first = allExistingArgs()
|
||||
.filter(it -> it.startsWith(opt) || it.equals(arg))
|
||||
.findFirst();
|
||||
if (first.isPresent()) {
|
||||
String overrideArg = first.get();
|
||||
if (!overrideArg.equals(arg)) {
|
||||
LOG.info("Default option '" + arg + "' is suppressed by '" + overrideArg + "'");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
raw.add(new Item(arg, parse));
|
||||
}
|
||||
@@ -151,12 +185,9 @@ public final class CommandBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
for (Item item : raw) {
|
||||
if (item.arg.equals(arg)) {
|
||||
continue loop;
|
||||
}
|
||||
if (allExistingArgs().noneMatch(arg::equals)) {
|
||||
raw.add(new Item(arg, parse));
|
||||
}
|
||||
raw.add(new Item(arg, parse));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,12 +196,12 @@ public final class CommandBuilder {
|
||||
}
|
||||
|
||||
private String addDefault(String opt, String value, boolean parse) {
|
||||
for (Item item : raw) {
|
||||
if (item.arg.startsWith(opt)) {
|
||||
LOG.info("Default option '" + opt + value + "' is suppressed by '" + item.arg + "'");
|
||||
return item.arg;
|
||||
}
|
||||
Optional<String> first = allExistingArgs().filter(arg -> arg.startsWith(opt)).findFirst();
|
||||
if (first.isPresent()) {
|
||||
LOG.info("Default option '" + opt + value + "' is suppressed by '" + first.get() + "'");
|
||||
return first.get();
|
||||
}
|
||||
|
||||
raw.add(new Item(opt + value, parse));
|
||||
return null;
|
||||
}
|
||||
@@ -189,6 +220,15 @@ public final class CommandBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
for (String arg : external) {
|
||||
final Matcher matcher = UNSTABLE_BOOLEAN_OPTION_PATTERN.matcher(arg);
|
||||
if (matcher.matches()) {
|
||||
if (matcher.group("key").equals(opt)) {
|
||||
return arg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value) {
|
||||
raw.add(new Item("-XX:+" + opt, parse));
|
||||
} else {
|
||||
@@ -211,6 +251,15 @@ public final class CommandBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
for (String arg : external) {
|
||||
final Matcher matcher = UNSTABLE_OPTION_PATTERN.matcher(arg);
|
||||
if (matcher.matches()) {
|
||||
if (matcher.group("key").equals(opt)) {
|
||||
return arg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
raw.add(new Item("-XX:" + opt + "=" + value, parse));
|
||||
return null;
|
||||
}
|
||||
@@ -302,6 +351,119 @@ public final class CommandBuilder {
|
||||
return containsEscape(s, " \t\"^&<>|") ? '"' + escape(s, '\\', '"') + '"' : s;
|
||||
}
|
||||
|
||||
/// Parses a Java command-line argument file content according to the specification:
|
||||
///
|
||||
/// [java Command-Line Argument Files](https://docs.oracle.com/en/java/javase/17/docs/specs/man/java.html#java-command-line-argument-files)
|
||||
static List<String> parseArgumentFile(Stream<String> lines) {
|
||||
List<String> result = new ArrayList<>();
|
||||
|
||||
// State across lines: when a quoted string spans multiple lines
|
||||
StringBuilder pending = null;
|
||||
char pendingQuote = 0;
|
||||
|
||||
for (Iterator<String> it = lines.iterator(); it.hasNext(); ) {
|
||||
String line = it.next();
|
||||
int len = line.length();
|
||||
int i = 0;
|
||||
|
||||
if (pending != null) {
|
||||
// We're continuing a multi-line quoted argument
|
||||
pending.append('\n');
|
||||
while (i < len) {
|
||||
char ch = line.charAt(i);
|
||||
if (ch == pendingQuote) {
|
||||
i++; // closing quote
|
||||
pendingQuote = 0;
|
||||
break;
|
||||
} else if (ch == '\\' && i + 1 < len) {
|
||||
pending.append(unescapeChar(line.charAt(++i)));
|
||||
i++;
|
||||
} else {
|
||||
pending.append(ch);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
if (pendingQuote != 0) {
|
||||
// Still not closed, continue to next line
|
||||
continue;
|
||||
}
|
||||
// Quote closed, fall through to continue parsing tokens on this line
|
||||
}
|
||||
|
||||
while (i < len) {
|
||||
char ch = line.charAt(i);
|
||||
|
||||
if (ch == ' ' || ch == '\t') {
|
||||
// Whitespace ends the current token (if any) or is skipped
|
||||
if (pending != null) {
|
||||
result.add(pending.toString());
|
||||
pending = null;
|
||||
}
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == '#' && pending == null) {
|
||||
// Comment - skip rest of line
|
||||
break;
|
||||
}
|
||||
|
||||
if (pending == null) {
|
||||
pending = new StringBuilder();
|
||||
}
|
||||
|
||||
if (ch == '\'' || ch == '"') {
|
||||
pendingQuote = ch;
|
||||
i++; // skip opening quote
|
||||
while (i < len) {
|
||||
ch = line.charAt(i);
|
||||
if (ch == pendingQuote) {
|
||||
pendingQuote = 0;
|
||||
i++;
|
||||
break;
|
||||
} else if (ch == '\\' && i + 1 < len) {
|
||||
pending.append(unescapeChar(line.charAt(++i)));
|
||||
i++;
|
||||
} else {
|
||||
pending.append(ch);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
if (pendingQuote != 0) {
|
||||
// Unclosed quote, continue on next line
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
pending.append(ch);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// End of line: if not inside a quote, finish the token
|
||||
if (pending != null && pendingQuote == 0) {
|
||||
result.add(pending.toString());
|
||||
pending = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle unclosed quote at end of file
|
||||
if (pending != null) {
|
||||
result.add(pending.toString());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static char unescapeChar(char ch) {
|
||||
return switch (ch) {
|
||||
case 'n' -> '\n';
|
||||
case 't' -> '\t';
|
||||
case 'r' -> '\r';
|
||||
case 'f' -> '\f';
|
||||
default -> ch;
|
||||
};
|
||||
}
|
||||
|
||||
public static String toShellStringLiteral(String s) {
|
||||
return containsEscape(s, " \t\"!#$&'()*,;<=>?[\\]^`{|}~") ? '"' + escape(s, '"', '$', '&', '`') + '"' : s;
|
||||
}
|
||||
|
||||
@@ -61,7 +61,28 @@ public final class NativeUtils {
|
||||
if (osVersion == null || osVersion.startsWith("5.") || osVersion.equals("6.0"))
|
||||
return false;
|
||||
|
||||
// Currently we only need to use JNA on Windows
|
||||
Native.getDefaultStringEncoding();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Platform.isMac()) {
|
||||
String osVersion = System.getProperty("os.version");
|
||||
|
||||
// Require macOS 10.14 or later
|
||||
if (osVersion != null) {
|
||||
String[] parts = osVersion.split("\\.");
|
||||
if (parts.length >= 2) {
|
||||
try {
|
||||
int major = Integer.parseInt(parts[0]);
|
||||
int minor = Integer.parseInt(parts[1]);
|
||||
if (major < 10 || (major == 10 && minor < 14)) {
|
||||
return false;
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Native.getDefaultStringEncoding();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.platform;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public record Platform(OperatingSystem os, Architecture arch) {
|
||||
public static final Platform UNKNOWN = new Platform(OperatingSystem.UNKNOWN, Architecture.UNKNOWN);
|
||||
|
||||
@@ -41,8 +44,23 @@ public record Platform(OperatingSystem os, Architecture arch) {
|
||||
public static final Platform CURRENT_PLATFORM = Platform.getPlatform(OperatingSystem.CURRENT_OS, Architecture.CURRENT_ARCH);
|
||||
public static final Platform SYSTEM_PLATFORM = Platform.getPlatform(OperatingSystem.CURRENT_OS, Architecture.SYSTEM_ARCH);
|
||||
|
||||
public static boolean isCompatibleWithX86Java() {
|
||||
return Architecture.SYSTEM_ARCH.isX86() || SYSTEM_PLATFORM.equals(MACOS_ARM64) || SYSTEM_PLATFORM.equals(WINDOWS_ARM64);
|
||||
private static final boolean SUPPORTED_TRANSLATION_X86_64;
|
||||
|
||||
static {
|
||||
if (SYSTEM_PLATFORM.equals(WINDOWS_ARM64)) {
|
||||
SUPPORTED_TRANSLATION_X86_64 = OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277;
|
||||
} else if (SYSTEM_PLATFORM.equals(MACOS_ARM64)) {
|
||||
SUPPORTED_TRANSLATION_X86_64 = Files.isRegularFile(Path.of("/usr/libexec/rosetta/runtime"));
|
||||
} else {
|
||||
SUPPORTED_TRANSLATION_X86_64 = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the current platform architecture is not x86-64,
|
||||
/// but can support executing x86-64 applications through translation;
|
||||
/// otherwise, returns `false`.
|
||||
public static boolean isSupportedTranslationX86_64() {
|
||||
return SUPPORTED_TRANSLATION_X86_64;
|
||||
}
|
||||
|
||||
public static Platform getPlatform(OperatingSystem os, Architecture arch) {
|
||||
|
||||
+38
-16
@@ -18,18 +18,22 @@
|
||||
package org.jackhuang.hmcl.util.platform.hardware;
|
||||
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.UnknownNullability;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
/// @author Glavo
|
||||
@NotNullByDefault
|
||||
public final class GraphicsCard {
|
||||
|
||||
public static String cleanName(String name) {
|
||||
@UnknownNullability
|
||||
public static String cleanName(@UnknownNullability String name) {
|
||||
if (name == null)
|
||||
return null;
|
||||
|
||||
@@ -58,13 +62,15 @@ public final class GraphicsCard {
|
||||
private final @Nullable Type type;
|
||||
private final @Nullable String driver;
|
||||
private final @Nullable String driverVersion;
|
||||
private final @Unmodifiable List<Path> vulkanDriverFiles;
|
||||
|
||||
private GraphicsCard(String name, @Nullable HardwareVendor vendor, @Nullable Type type, @Nullable String driver, @Nullable String driverVersion) {
|
||||
private GraphicsCard(String name, @Nullable HardwareVendor vendor, @Nullable Type type, @Nullable String driver, @Nullable String driverVersion, List<Path> vulkanDriverFiles) {
|
||||
this.name = Objects.requireNonNull(name);
|
||||
this.vendor = vendor;
|
||||
this.type = type;
|
||||
this.driver = driver;
|
||||
this.driverVersion = driverVersion;
|
||||
this.vulkanDriverFiles = vulkanDriverFiles;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
@@ -79,6 +85,10 @@ public final class GraphicsCard {
|
||||
return driverVersion;
|
||||
}
|
||||
|
||||
public List<Path> getVulkanDriverFiles() {
|
||||
return vulkanDriverFiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder(name);
|
||||
@@ -96,11 +106,12 @@ public final class GraphicsCard {
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private String name;
|
||||
private HardwareVendor vendor;
|
||||
private Type type;
|
||||
private String driver;
|
||||
private String driverVersion;
|
||||
private @Nullable String name;
|
||||
private @Nullable HardwareVendor vendor;
|
||||
private @Nullable Type type;
|
||||
private @Nullable String driver;
|
||||
private @Nullable String driverVersion;
|
||||
private @Nullable List<Path> vulkanDriverFiles;
|
||||
|
||||
public GraphicsCard build() {
|
||||
String name = this.name;
|
||||
@@ -111,10 +122,12 @@ public final class GraphicsCard {
|
||||
name = "Unknown";
|
||||
}
|
||||
|
||||
return new GraphicsCard(name, vendor, type, driver, driverVersion);
|
||||
List<Path> vulkanDriverFiles = Objects.requireNonNullElse(this.vulkanDriverFiles, List.of());
|
||||
|
||||
return new GraphicsCard(name, vendor, type, driver, driverVersion, List.copyOf(vulkanDriverFiles));
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
public @Nullable String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -123,7 +136,7 @@ public final class GraphicsCard {
|
||||
return this;
|
||||
}
|
||||
|
||||
public HardwareVendor getVendor() {
|
||||
public @Nullable HardwareVendor getVendor() {
|
||||
return vendor;
|
||||
}
|
||||
|
||||
@@ -132,7 +145,7 @@ public final class GraphicsCard {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
public @Nullable Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@@ -141,7 +154,7 @@ public final class GraphicsCard {
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getDriver() {
|
||||
public @Nullable String getDriver() {
|
||||
return driver;
|
||||
}
|
||||
|
||||
@@ -150,7 +163,7 @@ public final class GraphicsCard {
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getDriverVersion() {
|
||||
public @Nullable String getDriverVersion() {
|
||||
return driverVersion;
|
||||
}
|
||||
|
||||
@@ -158,5 +171,14 @@ public final class GraphicsCard {
|
||||
this.driverVersion = driverVersion;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @Nullable List<Path> getVulkanDriverFiles() {
|
||||
return vulkanDriverFiles;
|
||||
}
|
||||
|
||||
public Builder setVulkanDriverFiles(@Nullable List<Path> files) {
|
||||
this.vulkanDriverFiles = files;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.platform.macos;
|
||||
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
/// @author Glavo
|
||||
@NotNullByDefault
|
||||
public final class HomebrewUtils {
|
||||
|
||||
/// The path to Homebrew prefix.
|
||||
///
|
||||
/// - For macOS x86_64, it is `/usr/local`;
|
||||
/// - For macOS ARM64, it is `/opt/homebrew`;
|
||||
/// - For other operating systems, it is undefined.
|
||||
public static final Path HOMEBREW_PREFIX;
|
||||
|
||||
/// The path to `libvulkan.1.dylib`.
|
||||
///
|
||||
/// For non-macOS operating systems, it is undefined.
|
||||
public static final Path LIB_VULKAN;
|
||||
|
||||
static {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
// TODO: custom Homebrew prefix for macOS
|
||||
HOMEBREW_PREFIX = Architecture.SYSTEM_ARCH.isX86()
|
||||
? Path.of("/usr/local")
|
||||
: Path.of("/opt/homebrew");
|
||||
|
||||
LIB_VULKAN = HOMEBREW_PREFIX.resolve("lib/libvulkan.1.dylib");
|
||||
} else {
|
||||
// For other operating systems, we don't need Homebrew.
|
||||
var placeholder = Path.of("");
|
||||
HOMEBREW_PREFIX = placeholder;
|
||||
LIB_VULKAN = placeholder;
|
||||
}
|
||||
}
|
||||
|
||||
private HomebrewUtils() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.platform.macos;
|
||||
|
||||
import com.sun.jna.Library;
|
||||
import com.sun.jna.Pointer;
|
||||
import org.jackhuang.hmcl.util.platform.NativeUtils;
|
||||
|
||||
/// @see <a href="https://developer.apple.com/documentation/objectivec/objective-c-runtime">Objective-C Runtime</a>
|
||||
public interface ObjectiveCRuntime extends Library {
|
||||
|
||||
/// The Objective-C runtime library instance.
|
||||
ObjectiveCRuntime INSTANCE = NativeUtils.USE_JNA && com.sun.jna.Platform.isMac()
|
||||
? NativeUtils.load("objc", ObjectiveCRuntime.class)
|
||||
: null;
|
||||
|
||||
/// @see <a href="https://developer.apple.com/documentation/objectivec/objc_getclass(_:)">objc_getClass function</a>
|
||||
Pointer objc_getClass(String name);
|
||||
|
||||
/// @see <a href="https://developer.apple.com/documentation/objectivec/sel_registername(_:)">sel_registerName function</a>
|
||||
Pointer sel_registerName(String name);
|
||||
|
||||
/// @see <a href="https://developer.apple.com/documentation/ObjectiveC/objc_msgSend">objc_msgSend function</a>
|
||||
Pointer objc_msgSend(Pointer receiver, Pointer selector);
|
||||
|
||||
/// @see <a href="https://developer.apple.com/documentation/ObjectiveC/objc_msgSend">objc_msgSend function</a>
|
||||
Pointer objc_msgSend(Pointer receiver, Pointer selector, Pointer arg);
|
||||
|
||||
/// @see <a href="https://developer.apple.com/documentation/ObjectiveC/objc_msgSend">objc_msgSend function</a>
|
||||
Pointer objc_msgSend(Pointer receiver, Pointer selector, String arg);
|
||||
}
|
||||
+25
@@ -28,8 +28,11 @@ import org.jetbrains.annotations.Nullable;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@@ -48,6 +51,25 @@ final class WindowsGPUDetector {
|
||||
}
|
||||
}
|
||||
|
||||
private static @Nullable List<Path> parseVulkanDriverFiles(Object vulkanDriverFiles) {
|
||||
if (vulkanDriverFiles instanceof String file) {
|
||||
try {
|
||||
return List.of(Path.of(file));
|
||||
} catch (InvalidPathException e) {
|
||||
LOG.warning("Failed to parse Vulkan ICD file path: " + file, e);
|
||||
return null;
|
||||
}
|
||||
} else if (vulkanDriverFiles instanceof String[] files) {
|
||||
try {
|
||||
return Stream.of(files).map(Path::of).toList();
|
||||
} catch (InvalidPathException e) {
|
||||
LOG.warning("Failed to parse Vulkan ICD file paths: " + Arrays.toString(files), e);
|
||||
return null;
|
||||
}
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
||||
private static List<GraphicsCard> detectByCim() {
|
||||
try {
|
||||
String getCimInstance = OperatingSystem.SYSTEM_VERSION.getVersion().startsWith("6.1")
|
||||
@@ -116,6 +138,7 @@ final class WindowsGPUDetector {
|
||||
String vendor = regValueToString(reg.queryValue(hkey, subkey, "ProviderName"));
|
||||
String driverVersion = regValueToString(reg.queryValue(hkey, subkey, "DriverVersion"));
|
||||
String dacType = regValueToString(reg.queryValue(hkey, subkey, "HardwareInformation.DacType"));
|
||||
Object vulkanDriverName = reg.queryValue(hkey, subkey, "VulkanDriverName");
|
||||
|
||||
GraphicsCard.Builder builder = GraphicsCard.builder();
|
||||
if (name != null)
|
||||
@@ -126,6 +149,8 @@ final class WindowsGPUDetector {
|
||||
builder.setDriverVersion(driverVersion);
|
||||
if (dacType != null)
|
||||
builder.setType(fromDacType(dacType));
|
||||
if (vulkanDriverName != null)
|
||||
builder.setVulkanDriverFiles(parseVulkanDriverFiles(vulkanDriverName));
|
||||
result.add(builder.build());
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -95,6 +95,18 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
|
||||
return VersionRange.atMost(asGameVersion(maximum));
|
||||
}
|
||||
|
||||
/// Determines whether the given version string corresponds to a known Minecraft version.
|
||||
///
|
||||
/// If the version string cannot be parsed as any known version type (release, snapshot, pre-release, etc.)
|
||||
/// and is not a known April Fools snapshot, it is considered an unknown version.
|
||||
///
|
||||
/// @param version the version string to check, e.g. `"1.21.4"`, `"25w14craftmine"`, etc.
|
||||
/// @return {@code true} if the version string corresponds to a known Minecraft version, {@code false} otherwise
|
||||
/// @see #asGameVersion(String)
|
||||
public static boolean isKnown(String version) {
|
||||
return !(asGameVersion(version) instanceof Special special && special.prev == null);
|
||||
}
|
||||
|
||||
final String value;
|
||||
final String normalized;
|
||||
|
||||
@@ -151,7 +163,7 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
|
||||
///
|
||||
/// ```java
|
||||
/// GameVersionNumber.asVersion("...").isAtLeast("1.13", "17w43a");
|
||||
///```
|
||||
/// ```
|
||||
///
|
||||
/// @param strictReleaseVersion When `strictReleaseVersion` is `false`, `releaseVersion` is considered less than
|
||||
/// its corresponding pre/rc versions.
|
||||
|
||||
@@ -914,4 +914,6 @@
|
||||
26.1
|
||||
26.1.1-rc-1
|
||||
26.1.1
|
||||
26.1.2-rc-1
|
||||
26.1.2
|
||||
26w14a
|
||||
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
* 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.platform;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public final class CommandBuilderTest {
|
||||
|
||||
@Nested
|
||||
final class ParseArgumentFileTest {
|
||||
private List<String> parse(String... lines) {
|
||||
return CommandBuilder.parseArgumentFile(Arrays.stream(lines));
|
||||
}
|
||||
|
||||
// Basic: simple arguments separated by whitespace
|
||||
@Test
|
||||
public void testSimpleArguments() {
|
||||
assertEquals(List.of("-Xmx512m", "-Xms256m"), parse("-Xmx512m -Xms256m"));
|
||||
}
|
||||
|
||||
// Arguments on separate lines
|
||||
@Test
|
||||
public void testMultipleLines() {
|
||||
assertEquals(List.of("-Xmx512m", "-Xms256m", "-cp", "lib.jar"),
|
||||
parse("-Xmx512m", "-Xms256m", "-cp", "lib.jar"));
|
||||
}
|
||||
|
||||
// Tab-separated arguments
|
||||
@Test
|
||||
public void testTabSeparator() {
|
||||
assertEquals(List.of("-a", "-b"), parse("-a\t-b"));
|
||||
}
|
||||
|
||||
// Empty lines are ignored
|
||||
@Test
|
||||
public void testEmptyLines() {
|
||||
assertEquals(List.of("-a", "-b"), parse("-a", "", "", "-b"));
|
||||
}
|
||||
|
||||
// Comments: lines starting with #
|
||||
@Test
|
||||
public void testCommentLine() {
|
||||
assertEquals(List.of("-a"), parse("# this is a comment", "-a"));
|
||||
}
|
||||
|
||||
// Inline comment
|
||||
@Test
|
||||
public void testInlineComment() {
|
||||
assertEquals(List.of("-a"), parse("-a # comment"));
|
||||
}
|
||||
|
||||
// # inside quotes is not a comment
|
||||
@Test
|
||||
public void testHashInQuotes() {
|
||||
assertEquals(List.of("-a#b"), parse("\"-a#b\""));
|
||||
}
|
||||
|
||||
// Double-quoted string with spaces
|
||||
@Test
|
||||
public void testDoubleQuotedSpaces() {
|
||||
assertEquals(List.of("c:\\Program Files"), parse("\"c:\\\\Program Files\""));
|
||||
}
|
||||
|
||||
// Single-quoted string with spaces
|
||||
@Test
|
||||
public void testSingleQuotedSpaces() {
|
||||
assertEquals(List.of("hello world"), parse("'hello world'"));
|
||||
}
|
||||
|
||||
// Escape sequences inside quotes: \n \t \r \f
|
||||
@Test
|
||||
public void testEscapeSequencesInQuotes() {
|
||||
assertEquals(List.of("a\nb"), parse("\"a\\nb\""));
|
||||
assertEquals(List.of("a\tb"), parse("\"a\\tb\""));
|
||||
assertEquals(List.of("a\rb"), parse("\"a\\rb\""));
|
||||
assertEquals(List.of("a\fb"), parse("\"a\\fb\""));
|
||||
}
|
||||
|
||||
// Escaped backslash
|
||||
@Test
|
||||
public void testEscapedBackslash() {
|
||||
assertEquals(List.of("a\\b"), parse("\"a\\\\b\""));
|
||||
}
|
||||
|
||||
// Backslash outside quotes is treated literally
|
||||
@Test
|
||||
public void testBackslashOutsideQuotesBeforeWhitespace() {
|
||||
assertEquals(List.of("a\\", "b"), parse("a\\ b"));
|
||||
}
|
||||
|
||||
// Partial quote: concatenation of quoted and unquoted parts
|
||||
@Test
|
||||
public void testPartialQuote() {
|
||||
// c:\Program" "Files -> c:\Program Files
|
||||
assertEquals(List.of("c:\\Program Files"), parse("c:\\Program\" \"Files"));
|
||||
}
|
||||
|
||||
// Multi-line quoted string
|
||||
@Test
|
||||
public void testMultiLineQuotedString() {
|
||||
assertEquals(List.of("line1\nline2"), parse("\"line1", "line2\""));
|
||||
}
|
||||
|
||||
// Unclosed quote at EOF
|
||||
@Test
|
||||
public void testUnclosedQuoteAtEOF() {
|
||||
assertEquals(List.of("hello"), parse("\"hello"));
|
||||
}
|
||||
|
||||
// Line continuation with backslash at end of line
|
||||
// "An open quote stops at end-of-line unless \ is the last character,
|
||||
// which then joins the next line by removing all leading white space characters."
|
||||
// Note: the current implementation doesn't trim leading whitespace on continuation,
|
||||
// but let's test what the implementation actually does.
|
||||
@Test
|
||||
public void testMultiLineQuoteWithBackslashContinuation() {
|
||||
// Inside a quote, backslash at end of line: next char is first char of next line
|
||||
// "hello\
|
||||
// world" -> based on spec, leading whitespace should be trimmed
|
||||
// But the current impl treats \ followed by newline literally (unescapeChar('\n') -> '\n' stays as... wait no)
|
||||
// Actually looking at code: if ch == '\\' && i+1 < len, it reads next char.
|
||||
// If \ is at end of line (i+1 == len), the condition fails, so \ is appended literally.
|
||||
// Then the next line continues the multi-line quote with \n prepended.
|
||||
// Let me just test the actual behavior.
|
||||
List<String> result = parse("\"hello\\", "world\"");
|
||||
// \ at end of line: i+1 == len, so condition (ch == '\\' && i + 1 < len) is false
|
||||
// so '\\' is appended literally, then line ends, pendingQuote != 0
|
||||
// next line: \n is prepended, then "world" is read, then closing quote
|
||||
assertEquals(List.of("hello\\\nworld"), result);
|
||||
}
|
||||
|
||||
// Multiple arguments on one line with quotes
|
||||
@Test
|
||||
public void testMixedQuotedAndUnquoted() {
|
||||
assertEquals(List.of("-cp", "my path/lib.jar", "-Xmx1g"),
|
||||
parse("-cp \"my path/lib.jar\" -Xmx1g"));
|
||||
}
|
||||
|
||||
// Empty string via quotes
|
||||
@Test
|
||||
public void testEmptyQuotedString() {
|
||||
assertEquals(List.of("", "-a"), parse("\"\" -a"));
|
||||
}
|
||||
|
||||
// Only whitespace lines
|
||||
@Test
|
||||
public void testOnlyWhitespace() {
|
||||
assertEquals(List.of(), parse(" ", "\t", " \t "));
|
||||
}
|
||||
|
||||
// Empty input
|
||||
@Test
|
||||
public void testEmptyInput() {
|
||||
assertEquals(List.of(), parse());
|
||||
}
|
||||
|
||||
// Single-quoted escape sequences
|
||||
@Test
|
||||
public void testEscapeInSingleQuotes() {
|
||||
assertEquals(List.of("a\nb"), parse("'a\\nb'"));
|
||||
}
|
||||
|
||||
// Backslash outside quotes does not escape regular characters
|
||||
@Test
|
||||
public void testEscapeRegularCharOutsideQuotes() {
|
||||
assertEquals(List.of("a\\bc"), parse("a\\bc"));
|
||||
}
|
||||
|
||||
// Multiple tokens with various separators
|
||||
@Test
|
||||
public void testVariousSeparators() {
|
||||
assertEquals(List.of("a", "b", "c"), parse("a b\t\tc"));
|
||||
}
|
||||
|
||||
// @ prefix in argument file (just a regular argument, not expanded by parseArgumentFile)
|
||||
@Test
|
||||
public void testAtPrefix() {
|
||||
assertEquals(List.of("@file"), parse("@file"));
|
||||
}
|
||||
|
||||
// Realistic JVM argument file
|
||||
@Test
|
||||
public void testRealisticArgumentFile() {
|
||||
List<String> result = parse(
|
||||
"# JVM Options",
|
||||
"-Xmx2G",
|
||||
"-Xms512M",
|
||||
"-XX:+UseG1GC",
|
||||
"-Duser.home=\"/home/my user\"",
|
||||
"-cp",
|
||||
"\"lib/game.jar:lib/util.jar\""
|
||||
);
|
||||
assertEquals(List.of(
|
||||
"-Xmx2G",
|
||||
"-Xms512M",
|
||||
"-XX:+UseG1GC",
|
||||
"-Duser.home=/home/my user",
|
||||
"-cp",
|
||||
"lib/game.jar:lib/util.jar"
|
||||
), result);
|
||||
}
|
||||
|
||||
// Comment after whitespace (not at column 0, but after a token is null)
|
||||
@Test
|
||||
public void testCommentWithLeadingWhitespace() {
|
||||
assertEquals(List.of(), parse(" # comment"));
|
||||
}
|
||||
|
||||
// Quoted string immediately followed by unquoted text
|
||||
@Test
|
||||
public void testQuotedFollowedByUnquoted() {
|
||||
assertEquals(List.of("helloworld"), parse("\"hello\"world"));
|
||||
}
|
||||
|
||||
// Unquoted text followed by quoted text
|
||||
@Test
|
||||
public void testUnquotedFollowedByQuoted() {
|
||||
assertEquals(List.of("helloworld"), parse("hello\"world\""));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,4 +15,4 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
versionRoot=3.13
|
||||
versionRoot=3.14
|
||||
+29
-13
@@ -47,23 +47,39 @@ Users can enable the "Preview HMCL releases early" option on the "Settings > Gen
|
||||
## Release Model
|
||||
|
||||
```mermaid
|
||||
---
|
||||
displayMode: compact
|
||||
---
|
||||
gantt
|
||||
title HMCL Version Lifecycle (Example)
|
||||
section 3.9
|
||||
Beta Phase: a1, 2025-11-15, 30d
|
||||
Preview Phase: a2, after a1, 16d
|
||||
Stable Release: milestone, after a2, 0d
|
||||
Maintenance Phase: a3, after a2, 31d
|
||||
section 3.10
|
||||
Beta Phase: b1, after a1, 31d
|
||||
Preview Phase: b2, after b1, 16d
|
||||
Stable Release: milestone, after b2, 0d
|
||||
Maintenance Phase: b3, after b2, 31d
|
||||
section 3.11
|
||||
Beta Phase: c1, after b1, 31d
|
||||
Preview Phase: c2, after c1, 16d
|
||||
Stable Release: milestone, after c2, 0d
|
||||
today: vert, 2026-02-20, 0d
|
||||
Beta Phase: done, c1, after b1, 31d
|
||||
Preview Phase: active, c2, after c1, 16d
|
||||
Maintenance Phase: c3, after c2, 30d
|
||||
3.11.0.118: milestone, after b1, 0d
|
||||
3.11.0.119: milestone, 2026-02-01, 0d
|
||||
3.11.1: milestone, after c1, 0d
|
||||
section 3.10
|
||||
Beta Phase: done, b1, after a1, 31d
|
||||
Preview Phase: done, b2, after b1, 16d
|
||||
Maintenance Phase: active, b3, after b2, 31d
|
||||
3.10.0.116: milestone, after a1, 0d
|
||||
3.10.0.117: milestone, 2025-12-30, 0d
|
||||
3.10.1: milestone, after b1, 0d
|
||||
3.10.2: milestone, 2026-01-23, 0d
|
||||
3.10.3: milestone, after b2, 0d
|
||||
3.10.4: milestone, 2026-02-11, 0d
|
||||
section 3.9
|
||||
Beta Phase: done, a1, 2025-11-15, 30d
|
||||
Preview Phase: done, a2, after a1, 16d
|
||||
Maintenance Phase: done, a3, after a2, 31d
|
||||
3.9.0.114: milestone, 2025-11-15, 0d
|
||||
3.9.0.115: milestone, 2025-11-30, 0d
|
||||
3.9.1: milestone, after a1, 0d
|
||||
3.9.2: milestone, 2025-12-23, 0d
|
||||
3.9.3: milestone, after a2, 0d
|
||||
3.9.4: milestone, 2026-01-11, 0d
|
||||
```
|
||||
|
||||
In general, we release a new version branch every month,
|
||||
|
||||
+29
-13
@@ -41,23 +41,39 @@ HMCL 具有两个主要发布通道:**稳定版通道**和**开发版通道**
|
||||
## 发布模型
|
||||
|
||||
```mermaid
|
||||
---
|
||||
displayMode: compact
|
||||
---
|
||||
gantt
|
||||
title HMCL 版本生命周期 (示例)
|
||||
section 3.9
|
||||
开发阶段: a1, 2025-11-15, 30d
|
||||
预览阶段: a2, after a1, 16d
|
||||
稳定版发布: milestone, after a2, 0d
|
||||
维护阶段: a3, after a2, 31d
|
||||
section 3.10
|
||||
开发阶段: b1, after a1, 31d
|
||||
预览阶段: b2, after b1, 16d
|
||||
稳定版发布: milestone, after b2, 0d
|
||||
维护阶段: b3, after b2, 31d
|
||||
section 3.11
|
||||
开发阶段: c1, after b1, 31d
|
||||
预览阶段: c2, after c1, 16d
|
||||
稳定版发布: milestone, after c2, 0d
|
||||
today: vert, 2026-02-20, 0d
|
||||
开发阶段: done, c1, after b1, 31d
|
||||
预览阶段: active, c2, after c1, 16d
|
||||
维护阶段: c3, after c2, 30d
|
||||
3.11.0.118: milestone, after b1, 0d
|
||||
3.11.0.119: milestone, 2026-02-01, 0d
|
||||
3.11.1: milestone, after c1, 0d
|
||||
section 3.10
|
||||
开发阶段: done, b1, after a1, 31d
|
||||
预览阶段: done, b2, after b1, 16d
|
||||
维护阶段: active, b3, after b2, 31d
|
||||
3.10.0.116: milestone, after a1, 0d
|
||||
3.10.0.117: milestone, 2025-12-30, 0d
|
||||
3.10.1: milestone, after b1, 0d
|
||||
3.10.2: milestone, 2026-01-23, 0d
|
||||
3.10.3: milestone, after b2, 0d
|
||||
3.10.4: milestone, 2026-02-11, 0d
|
||||
section 3.9
|
||||
开发阶段: done, a1, 2025-11-15, 30d
|
||||
预览阶段: done, a2, after a1, 16d
|
||||
维护阶段: done, a3, after a2, 31d
|
||||
3.9.0.114: milestone, 2025-11-15, 0d
|
||||
3.9.0.115: milestone, 2025-11-30, 0d
|
||||
3.9.1: milestone, after a1, 0d
|
||||
3.9.2: milestone, 2025-12-23, 0d
|
||||
3.9.3: milestone, after a2, 0d
|
||||
3.9.4: milestone, 2026-01-11, 0d
|
||||
```
|
||||
|
||||
通常情况下,我们每个月发布一个版本分支,每个版本分支 `x.y` 生命周期为两个半月。
|
||||
|
||||
@@ -41,23 +41,39 @@ HMCL 具有兩個主要發布管道:**穩定版管道**和**開發版管道**
|
||||
## 發布模型
|
||||
|
||||
```mermaid
|
||||
---
|
||||
displayMode: compact
|
||||
---
|
||||
gantt
|
||||
title HMCL 版本生命週期 (範例)
|
||||
section 3.9
|
||||
開發階段: a1, 2025-11-15, 30d
|
||||
預覽階段: a2, after a1, 16d
|
||||
穩定版發布: milestone, after a2, 0d
|
||||
維護階段: a3, after a2, 31d
|
||||
section 3.10
|
||||
開發階段: b1, after a1, 31d
|
||||
預覽階段: b2, after b1, 16d
|
||||
穩定版發布: milestone, after b2, 0d
|
||||
維護階段: b3, after b2, 31d
|
||||
section 3.11
|
||||
開發階段: c1, after b1, 31d
|
||||
預覽階段: c2, after c1, 16d
|
||||
穩定版發布: milestone, after c2, 0d
|
||||
today: vert, 2026-02-20, 0d
|
||||
開發階段: done, c1, after b1, 31d
|
||||
預覽階段: active, c2, after c1, 16d
|
||||
維護階段: c3, after c2, 30d
|
||||
3.11.0.118: milestone, after b1, 0d
|
||||
3.11.0.119: milestone, 2026-02-01, 0d
|
||||
3.11.1: milestone, after c1, 0d
|
||||
section 3.10
|
||||
開發階段: done, b1, after a1, 31d
|
||||
預覽階段: done, b2, after b1, 16d
|
||||
維護階段: active, b3, after b2, 31d
|
||||
3.10.0.116: milestone, after a1, 0d
|
||||
3.10.0.117: milestone, 2025-12-30, 0d
|
||||
3.10.1: milestone, after b1, 0d
|
||||
3.10.2: milestone, 2026-01-23, 0d
|
||||
3.10.3: milestone, after b2, 0d
|
||||
3.10.4: milestone, 2026-02-11, 0d
|
||||
section 3.9
|
||||
開發階段: done, a1, 2025-11-15, 30d
|
||||
預覽階段: done, a2, after a1, 16d
|
||||
維護階段: done, a3, after a2, 31d
|
||||
3.9.0.114: milestone, 2025-11-15, 0d
|
||||
3.9.0.115: milestone, 2025-11-30, 0d
|
||||
3.9.1: milestone, after a1, 0d
|
||||
3.9.2: milestone, 2025-12-23, 0d
|
||||
3.9.3: milestone, after a2, 0d
|
||||
3.9.4: milestone, 2026-01-11, 0d
|
||||
```
|
||||
|
||||
通常情況下,我們每個月發布一個版本分支,每個版本分支 `x.y` 生命週期為兩個半月。
|
||||
|
||||
@@ -4,7 +4,7 @@ jetbrains-annotations = "26.1.0"
|
||||
kala-compress = "1.27.1-3"
|
||||
simple-png-javafx = "0.3.0"
|
||||
gson = "2.13.2"
|
||||
toml4j = "0.7.2"
|
||||
tomlj = "1.1.1"
|
||||
xz = "1.12"
|
||||
lz4 = "1.10.4.1"
|
||||
fx-gson = "5.0.0"
|
||||
@@ -13,7 +13,6 @@ hello-nbt = "0.3.0"
|
||||
nanohttpd = "2.3.1"
|
||||
jsoup = "1.21.2"
|
||||
chardet = "2.5.0"
|
||||
twelvemonkeys = "3.13.1"
|
||||
fxsvgimage = "1.3"
|
||||
jna = "5.18.1"
|
||||
pci-ids = "0.4.0"
|
||||
@@ -23,6 +22,7 @@ lwjgl-unsafe-agent = "2.0"
|
||||
monet-fx = "0.4.0"
|
||||
terracotta = "0.4.2"
|
||||
nayuki-qrcodegen = "1.8.0"
|
||||
jwebp = "0.2.0"
|
||||
|
||||
# testing
|
||||
junit = "6.0.1"
|
||||
@@ -38,7 +38,7 @@ kala-compress-zip = { module = "org.glavo.kala:kala-compress-archivers-zip", ver
|
||||
kala-compress-tar = { module = "org.glavo.kala:kala-compress-archivers-tar", version.ref = "kala-compress" }
|
||||
simple-png-javafx = { module = "org.glavo:simple-png-javafx", version.ref = "simple-png-javafx" }
|
||||
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
|
||||
toml = { module = "com.moandjiezana.toml:toml4j", version.ref = "toml4j" }
|
||||
tomlj = { module = "org.tomlj:tomlj", version.ref = "tomlj" }
|
||||
xz = { module = "org.tukaani:xz", version.ref = "xz" }
|
||||
lz4 = { module = "org.glavo:lz4-java", version.ref = "lz4" }
|
||||
fx-gson = { module = "org.hildan.fxgson:fx-gson", version.ref = "fx-gson" }
|
||||
@@ -47,7 +47,6 @@ hello-nbt = { module = "org.glavo:HelloNBT", version.ref = "hello-nbt" }
|
||||
nanohttpd = { module = "org.nanohttpd:nanohttpd", version.ref = "nanohttpd" }
|
||||
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
|
||||
chardet = { module = "org.glavo:chardet", version.ref = "chardet" }
|
||||
twelvemonkeys-imageio-webp = { module = "com.twelvemonkeys.imageio:imageio-webp", version.ref = "twelvemonkeys" }
|
||||
fxsvgimage = { module = "com.github.hervegirod:fxsvgimage", version.ref = "fxsvgimage" }
|
||||
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
|
||||
jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna" }
|
||||
@@ -57,6 +56,7 @@ authlib-injector = { module = "org.glavo.hmcl:authlib-injector", version.ref = "
|
||||
lwjgl-unsafe-agent = { module = "org.glavo:lwjgl-unsafe-agent", version.ref = "lwjgl-unsafe-agent" }
|
||||
monet-fx = { module = "org.glavo:MonetFX", version.ref = "monet-fx" }
|
||||
nayuki-qrcodegen = { module = "io.nayuki:qrcodegen", version.ref = "nayuki-qrcodegen" }
|
||||
jwebp = { module = "org.glavo:webp", version.ref = "jwebp" }
|
||||
|
||||
# testing
|
||||
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
|
||||
|
||||
Reference in new issue
Block a user