Compare commits

..
60 changed files with 327 additions and 1576 deletions

No files matched your search

-2
View File
@@ -69,5 +69,3 @@ language-subtag-registry
# AI
/.gemini/
!/.gemini/config.yaml
.codex
/.gradle-*/
-27
View File
@@ -1,27 +0,0 @@
# 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.
+2 -2
View File
@@ -56,7 +56,7 @@ dependencies {
implementation(project(":HMCLCore"))
implementation(project(":HMCLBoot"))
implementation("libs:JFoenix")
implementation(libs.jwebp)
implementation(libs.twelvemonkeys.imageio.webp)
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-*",
"aix-*", "sunos-*", "openbsd-*", "dragonflybsd-*", "freebsd-*", "linux-*", "darwin-*",
"*-ppc", "*-ppc64le", "*-s390x", "*-armel",
).forEach { exclude("com/sun/jna/$it/**") }
@@ -1,277 +0,0 @@
//
// 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,7 +39,6 @@ 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;
@@ -139,9 +138,6 @@ 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,6 +24,7 @@ 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;
/**
@@ -59,38 +60,34 @@ 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 = Path.of(System.getProperty("user.dir")).toAbsolutePath().normalize();
public static final Path CURRENT_DIRECTORY = Paths.get(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", System.getenv("HMCL_USER_HOME"));
if (StringUtils.isBlank(hmclHome)) {
String hmclHome = System.getProperty("hmcl.home");
if (hmclHome == null) {
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
String xdgData = System.getenv("XDG_DATA_HOME");
if (StringUtils.isNotBlank(xdgData)) {
HMCL_GLOBAL_DIRECTORY = Path.of(xdgData, "hmcl").toAbsolutePath().normalize();
HMCL_GLOBAL_DIRECTORY = Paths.get(xdgData, "hmcl").toAbsolutePath().normalize();
} else {
HMCL_GLOBAL_DIRECTORY = Path.of(System.getProperty("user.home"), ".local", "share", "hmcl").toAbsolutePath().normalize();
HMCL_GLOBAL_DIRECTORY = Paths.get(System.getProperty("user.home"), ".local", "share", "hmcl").toAbsolutePath().normalize();
}
} else {
HMCL_GLOBAL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
}
} else {
HMCL_GLOBAL_DIRECTORY = Path.of(hmclHome).toAbsolutePath().normalize();
HMCL_GLOBAL_DIRECTORY = Paths.get(hmclHome).toAbsolutePath().normalize();
}
String hmclCurrentDir = System.getProperty("hmcl.dir", System.getenv("HMCL_LOCAL_HOME"));
HMCL_CURRENT_DIRECTORY = StringUtils.isNotBlank(hmclCurrentDir)
? Path.of(hmclCurrentDir).toAbsolutePath().normalize()
String hmclCurrentDir = System.getProperty("hmcl.dir");
HMCL_CURRENT_DIRECTORY = hmclCurrentDir != null
? Paths.get(hmclCurrentDir).toAbsolutePath().normalize()
: CURRENT_DIRECTORY.resolve(".hmcl");
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");
DEPENDENCIES_DIRECTORY = HMCL_CURRENT_DIRECTORY.resolve("dependencies");
}
public static boolean isStable() {
@@ -515,7 +515,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
FORBIDDEN_VERSION_IDS.contains(id.toLowerCase(Locale.ROOT)))
return false;
return FileUtils.isNameValidForJar(id);
return FileUtils.isNameValid(id);
}
/**
@@ -72,6 +72,7 @@ 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 {
@@ -624,7 +625,7 @@ public final class LauncherHelper {
break;
case VANILLA_X86:
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER
&& Platform.isSupportedTranslationX86_64()) {
&& isCompatibleWithX86Java()) {
suggestions.add(i18n("launch.advice.vanilla_x86.translation"));
}
break;
@@ -31,6 +31,7 @@ public final class Log {
private final String log;
private Log4jLevel level;
private boolean selected = false;
public Log(String log) {
this.log = log;
@@ -56,6 +57,14 @@ 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,27 +122,16 @@ 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) {
// 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;
}
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
return OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277 && architecture == Architecture.X86_64 || architecture == Architecture.X86;
break;
case LINUX:
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
return architecture == Architecture.X86;
break;
case MACOS:
// macOS Arm64 compatible with x86-64 programs via Rosetta 2.
if (Architecture.SYSTEM_ARCH == Architecture.ARM64 && Platform.isSupportedTranslationX86_64())
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
return architecture == Architecture.X86_64;
break;
}
@@ -375,13 +364,13 @@ public final class JavaManager {
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
searcher.searchAllJavaInRepository(Platform.WINDOWS_X86);
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
if (Platform.isSupportedTranslationX86_64())
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277)
searcher.searchAllJavaInRepository(Platform.WINDOWS_X86_64);
searcher.searchAllJavaInRepository(Platform.WINDOWS_X86);
}
break;
case MACOS:
if (Architecture.SYSTEM_ARCH == Architecture.ARM64 && Platform.isSupportedTranslationX86_64())
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
searcher.searchAllJavaInRepository(Platform.MACOS_X86_64);
break;
}
@@ -795,15 +784,13 @@ 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 (Platform.isSupportedTranslationX86_64()) {
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277) {
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.WINDOWS_X86_64), verify);
}
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.WINDOWS_X86), 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);
}
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && Architecture.CURRENT_ARCH == Architecture.ARM64) {
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.MACOS_X86_64), verify);
}
}
@@ -36,9 +36,8 @@ 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.ui.MacOSNativeUtils;
import org.jackhuang.hmcl.ui.WindowsNativeUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.ui.WindowsNativeUtils;
import org.jackhuang.hmcl.util.platform.NativeUtils;
import org.jackhuang.hmcl.util.platform.OSVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
@@ -234,11 +233,6 @@ 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,21 +213,6 @@ 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) {
@@ -390,7 +375,7 @@ public final class Controllers {
Runnable continueAction = () -> globalConfig().setPlatformPromptVersion(1);
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && Architecture.SYSTEM_ARCH == Architecture.ARM64) {
continueAction.run();
Controllers.dialog(i18n("fatal.unsupported_platform.macos_arm64"), null, MessageType.INFO, continueAction);
} 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,7 +37,6 @@ 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())
@@ -58,7 +57,6 @@ public final class HTMLRenderer {
private boolean underline;
private boolean strike;
private boolean highlight;
private int indentLevel;
private String headerLevel;
private Node hyperlink;
@@ -220,7 +218,7 @@ public final class HTMLRenderer {
appendImage(node);
break;
case "li":
appendText("\n" + INDENT.repeat(indentLevel) + " \u2022 ");
appendText("\n \u2022 ");
break;
case "dt":
appendText(" ");
@@ -239,14 +237,11 @@ 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,6 +30,7 @@ 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;
@@ -38,6 +39,7 @@ 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;
@@ -259,6 +261,9 @@ 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) {
@@ -304,7 +309,6 @@ 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);
@@ -314,6 +318,7 @@ 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) {
@@ -323,6 +328,32 @@ 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
@@ -336,6 +367,7 @@ 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);
@@ -347,13 +379,10 @@ 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.getSelectionModel().getSelectedItems()) {
if (item != null) {
for (Log item : listView.getItems()) {
if (item != null && item.isSelected()) {
if (item.getLog() != null)
stringBuilder.append(item.getLog());
stringBuilder.append('\n');
@@ -1,101 +0,0 @@
/*
* 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() {
}
}
@@ -19,8 +19,6 @@ 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;
@@ -30,7 +28,6 @@ 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;
@@ -46,6 +43,8 @@ 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);
@@ -105,26 +104,12 @@ final class ComponentSublistWrapper extends VBox implements NoPaddingComponent {
if (container == null) {
this.container = new VBox();
this.container.getStyleClass().add("container");
if (!noPadding) {
container.setPadding(new Insets(8, 16, 10, 16));
}
FXUtils.setLimitHeight(container, 0);
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);
FXUtils.setOverflowHidden(container);
container.getChildren().setAll(sublist);
ComponentSublistWrapper.this.getChildren().add(container);
@@ -135,7 +120,8 @@ final class ComponentSublistWrapper extends VBox implements NoPaddingComponent {
}
Platform.runLater(() -> {
double contentHeight = expanded ? sublist.prefHeight(sublist.getWidth()) : 0;
// 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 targetRotate = expanded ? -180 : 0;
if (AnimationUtils.isAnimationEnabled()) {
@@ -17,14 +17,8 @@
*/
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;
@@ -32,10 +26,6 @@ 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
@@ -46,10 +36,6 @@ 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);
@@ -59,8 +45,8 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
Point2D dragStart;
double preDragThumbPos;
final NumberBinding range = Bindings.subtract(scrollBar.maxProperty(), scrollBar.minProperty());
final NumberBinding position = Bindings.divide(Bindings.subtract(scrollBar.valueProperty(), scrollBar.minProperty()), range);
NumberBinding range = Bindings.subtract(scrollBar.maxProperty(), scrollBar.minProperty());
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
@@ -77,6 +63,7 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
setup();
thumb.setOnMousePressed(me -> {
if (me.isSynthesized()) {
// touch-screen events handled by Scroll handler
@@ -141,18 +128,14 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
track.widthProperty().unbind();
track.heightProperty().unbind();
double offset = 4;
if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
track.relocate(offset, -6.0);
NumberBinding trackWidth = Bindings.max(0, Bindings.subtract(scrollBar.widthProperty(), offset * 2));
track.widthProperty().bind(trackWidth);
track.setHeight(6.0);
track.relocate(0, -8);
track.widthProperty().bind(scrollBar.widthProperty());
track.setHeight(8);
} else {
track.relocate(-6.0, offset);
track.setWidth(6.0);
NumberBinding trackHeight = Bindings.max(0, Bindings.subtract(scrollBar.heightProperty(), offset * 2));
track.heightProperty().bind(trackHeight);
track.relocate(-8, 0);
track.setWidth(8);
track.heightProperty().bind(scrollBar.heightProperty());
}
thumb.xProperty().unbind();
@@ -161,37 +144,15 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
thumb.heightProperty().unbind();
if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
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)
)
);
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));
} else {
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)
)
);
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));
}
}
@@ -201,7 +162,7 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
return Double.MAX_VALUE;
}
return 6.0;
return 8;
}
@Override
@@ -210,46 +171,13 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
return Double.MAX_VALUE;
}
return 6.0;
return 8;
}
};
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;
}
@@ -55,8 +55,7 @@ public final class MultiFileItem<T> extends VBox {
@SuppressWarnings("unchecked")
public MultiFileItem() {
this.getStyleClass().add("multi-file-item");
setPadding(new Insets(0, 0, 10, 0));
setSpacing(8);
group.selectedToggleProperty().addListener((a, b, newValue) -> {
@@ -81,11 +81,12 @@ public class TaskExecutorDialogPane extends BorderPane {
setCancel(cancel);
btnCancel.setDisable(onCancel.getCancellationAction() == null);
btnCancel.setOnAction(e -> {
if (executor != null)
executor.cancel();
onCancel.getCancellationAction().accept(this);
if (onCancel.getCancellationAction() != null) {
if (executor != null)
executor.cancel();
onCancel.getCancellationAction().accept(this);
}
});
speedEventHandler = FetchTask.SPEED_EVENT.registerWeak(speedEvent -> {
@@ -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::isNameValidForJar), new Validator(i18n("profile.already_exists"), (it) -> !finalExistingFiles.contains(it)));
}, file.getFile().getFilename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid), new Validator(i18n("profile.already_exists"), (it) -> !finalExistingFiles.contains(it)));
}
@@ -423,11 +423,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
if (status == Status.LOADING)
transitionPane.setContent(spinner, ContainerAnimations.FADE);
else if (status == Status.SUCCESS)
if (control.versions.isEmpty()) {
transitionPane.setContent(emptyPane, ContainerAnimations.FADE);
} else {
transitionPane.setContent(centerWrapper, ContainerAnimations.FADE);
}
transitionPane.setContent(centerWrapper, ContainerAnimations.FADE);
else // if (status == Status.FAILED)
transitionPane.setContent(failedPane, ContainerAnimations.FADE);
});
@@ -17,6 +17,7 @@
*/
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;
@@ -28,9 +29,6 @@ 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;
@@ -40,8 +38,13 @@ 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;
@@ -68,8 +71,16 @@ public final class ImageUtils {
};
public static final ImageLoader WEBP = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
var options = new WebPImageLoadOptions(requestedWidth, requestedHeight, preserveRatio, smooth);
return new WebPFXImage(WebPImage.read(input, options));
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);
};
public static final ImageLoader SVG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
@@ -457,19 +468,6 @@ 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() {
}
}
@@ -188,6 +188,9 @@ 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"));
@@ -28,7 +28,6 @@ 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;
@@ -98,14 +97,6 @@ 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();
@@ -132,13 +123,12 @@ 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,
selectAllButton,
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
createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () ->
listView.getSelectionModel().clearSelection())
);
@@ -194,7 +184,6 @@ 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,6 +531,7 @@ 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);
@@ -577,11 +578,7 @@ 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());
String description = item.getDescription();
if (description != null) {
description = description.replaceAll("\\R", " ");
}
content.setSubtitle(description);
content.setSubtitle(item.getDescription());
content.getTags().clear();
for (String category : item.getCategories()) {
if (getSkinnable().shouldDisplayCategory(category))
@@ -54,10 +54,7 @@ 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.FXThread;
import org.jackhuang.hmcl.util.Lazy;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
@@ -157,18 +154,6 @@ 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"), () -> {
@@ -186,7 +171,8 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
.toList()
)
),
selectAll,
createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () ->
listView.getSelectionModel().selectAll()),
createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () ->
listView.getSelectionModel().clearSelection())
);
@@ -351,6 +351,7 @@ 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,11 +264,6 @@ 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);
@@ -278,21 +273,6 @@ 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,15 +43,13 @@ 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.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class UpdateHandler {
private UpdateHandler() {
@@ -127,29 +125,6 @@ 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) {
@@ -93,10 +93,5 @@
"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"
}
]
+3 -19
View File
@@ -29,8 +29,8 @@
.scroll-bar .thumb {
-fx-fill: -monet-surface-tint;
-fx-arc-width: 6;
-fx-arc-height: 6;
-fx-arc-width: 8;
-fx-arc-height: 8;
}
.title-label {
@@ -1093,12 +1093,8 @@
-fx-background-color: -monet-error-container;
}
.multi-file-item {
-fx-padding: 10 0 10 0;
}
.options-sublist {
-fx-background-color: -monet-surface-container;
-fx-background-color: -monet-surface;
}
.options-list-item {
@@ -1109,14 +1105,6 @@
-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;
@@ -1152,10 +1140,6 @@
-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 & MCIM
download.provider.bmclapi.desc=This option is provided exclusively for users in Chinese Mainland.
download.provider.bmclapi=BMCLAPI
download.provider.bmclapi.desc=bangbang93, https://bmclapi2.bangbang93.com/
download.provider.mojang=Official
download.provider.mojang.desc=Note that OptiFine is provided by BMCLAPI.
download.provider.mojang.desc=OptiFine is provided by BMCLAPI
download.provider.official=From Official Sources
download.provider.official.desc=Always obtain from official sources.
download.provider.official.desc=Latest, but may load slowly
download.provider.balanced=From Fastest Available
download.provider.balanced.desc=Best choice for ALL users.
download.provider.balanced.desc=Balanced, but may not be the latest
download.provider.mirror=From Mirror
download.provider.mirror.desc=Recommended option for users in Chinese Mainland.
download.provider.mirror.desc=Fast, but may not be the latest
download.java=Downloading Java
download.java.process=Java Download Process
download.javafx=Downloading dependencies for the launcher...
@@ -452,6 +452,8 @@ 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\
@@ -1363,6 +1365,10 @@ 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
@@ -354,11 +354,18 @@ download.external_link=زيارة موقع التنزيل
download.failed=فشل تنزيل "%1$s"، رمز الاستجابة: %2$d.
download.failed.empty=لا توجد إصدارات متاحة. انقر هنا للرجوع.
download.failed.no_code=فشل التنزيل
download.failed.refresh=فشل في جلب قائمة الإصدارات. يرجى النقر هنا لإعادة المحاولة.
download.game=New Game
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.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=جارٍ تنزيل مكتبات المشغّل...
@@ -438,6 +445,8 @@ 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\
@@ -1332,6 +1341,10 @@ 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=أمر ما قبل التشغيل
@@ -336,6 +336,10 @@ 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
@@ -414,6 +418,8 @@ 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\
@@ -1245,6 +1251,9 @@ 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:
@@ -293,6 +293,10 @@ 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=ミラーソースからロード
@@ -774,6 +778,7 @@ 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=ゲーム開始前に実行されます
@@ -332,6 +332,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.mojang=官版
download.provider.mojang.desc=Optifine 猶取諸 BMCLAPI 也
download.provider.official=先官版
download.provider.official.desc=至新,容有艱
download.provider.balanced=先其快者
@@ -381,6 +385,7 @@ 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=案
@@ -1061,6 +1066,7 @@ 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=將前戲啟而行
@@ -333,6 +333,10 @@ 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=Из зеркала
@@ -408,6 +412,8 @@ 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\
@@ -1236,6 +1242,10 @@ 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=Команды, которые необходимо выполнить перед запуском игры
@@ -332,6 +332,10 @@ 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=З дзеркала
@@ -394,6 +398,8 @@ 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 та встановити пакет сумісності.
@@ -1184,6 +1190,9 @@ 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=Команди для виконання перед запуском гри
@@ -359,16 +359,16 @@ download.failed.empty=沒有可供安裝的版本,按一下此處返回。
download.failed.no_code=下載失敗
download.failed.refresh=載入版本清單失敗,按一下此處重試。
download.game=新遊戲
download.provider.bmclapi=BMCLAPI & MCIM
download.provider.bmclapi.desc=適用於中國大陸的鏡像源
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=選取載入速度快的下載來源
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,6 +419,7 @@ 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按下「確定」以繼續,但請注意,此操作可能會引發一些問題。
@@ -1154,6 +1155,7 @@ 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=遊戲啟動前執行指令
@@ -361,10 +361,10 @@ download.failed.empty=[没有可供安装的版本,点击此处返回]\n(你
download.failed.no_code=下载失败
download.failed.refresh=[加载版本列表失败,点击此处重试]\n(你可以点击右上角帮助按钮进行求助)
download.game=新游戏
download.provider.bmclapi=BMCLAPI & MCIM
download.provider.bmclapi.desc=适用于中国大陆的镜像源
download.provider.bmclapi=BMCLAPI
download.provider.bmclapi.desc=bangbang93, https://bmclapi2.bangbang93.com
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,6 +423,7 @@ 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按下“确定”以继续,但请注意,此操作可能会引发一些问题。
@@ -1159,6 +1160,7 @@ 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=游戏启动前执行命令
+1 -1
View File
@@ -16,7 +16,7 @@ dependencies {
api(libs.kala.compress.tar)
api(libs.simple.png.javafx)
api(libs.gson)
api(libs.tomlj)
api(libs.toml)
api(libs.xz)
api(libs.lz4)
api(libs.fx.gson)
@@ -88,15 +88,13 @@ public final class World {
root = fs.getPath("/");
fileName = FileUtils.getName(this.file);
} else {
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);
List<Path> files = Files.list(fs.getPath("/")).toList();
if (files.size() != 1 || !Files.isDirectory(files.get(0))) {
throw new IOException("Not a valid world zip file");
}
root = files.get(0);
fileName = FileUtils.getName(root);
}
Path levelDat = root.resolve("level.dat");
@@ -109,7 +109,7 @@ public class DefaultLauncher extends Launcher {
res.add(options.getJava().getBinary().toString());
res.addAllWithoutParsingAndReadExternal(options.getOverrideJavaArguments());
res.addAllWithoutParsing(options.getOverrideJavaArguments());
if (options.getMaxMemory() != null && options.getMaxMemory() > 0)
res.addDefault("-Xmx", options.getMaxMemory() + "m");
@@ -722,10 +722,10 @@ public class DefaultLauncher extends Launcher {
boolean usePowerShell = "ps1".equals(scriptExtension);
if (!usePowerShell) {
if (isWindows && !scriptExtension.equalsIgnoreCase("bat"))
if (isWindows && !scriptExtension.equals("bat"))
throw new IllegalArgumentException("The extension of " + scriptFile + " is not 'bat' or 'ps1' in Windows");
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");
else if (!isWindows && !(scriptExtension.equals("sh") || scriptExtension.equals("command")))
throw new IllegalArgumentException("The extension of " + scriptFile + " is not 'sh', 'ps1' or 'command' in macOS/Linux");
}
final Command commandLine = generateCommandLine(nativeFolder);
@@ -365,16 +365,14 @@ public final class ModManager {
}
/**
* Check if "mods" directory has mod file named "fileName" no matter the mod is disabled,upgraded or not
* Check if "mods" directory has mod file named "fileName" no matter the mod is disabled 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.removeSuffix(fileName, OLD_EXTENSION)))
|| Files.exists(getModsDirectory().resolve(StringUtils.addSuffix(fileName, OLD_EXTENSION)));
|| Files.exists(getModsDirectory().resolve(StringUtils.addSuffix(fileName, DISABLED_EXTENSION)));
}
public Path getSimpleModPath(String fileName) {
@@ -24,7 +24,6 @@ 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;
@@ -213,13 +212,7 @@ public class CurseAddon implements RemoteMod.IMod {
}
public RemoteMod toMod() {
String iconUrl = "";
if (logo != null) {
if (StringUtils.isNotBlank(logo.getThumbnailUrl()))
iconUrl = logo.getThumbnailUrl();
else if (StringUtils.isNotBlank(logo.getUrl()))
iconUrl = logo.getUrl();
}
String iconUrl = Optional.ofNullable(logo).map(Logo::getThumbnailUrl).orElse("");
return new RemoteMod(
slug,
@@ -188,13 +188,9 @@ public final class CurseCompletionTask extends Task<Void> {
String fileName = file.fileName();
return switch (classID) {
case 12, // Resource pack
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;
6552 -> { // Shader pack
Path res = (classID == 12 ? resourcePacksRoot : shaderPacksRoot).resolve(fileName);
yield Files.exists(res) ? null : res;
}
default -> {
if (modManager.hasSimpleMod(fileName)) {
@@ -24,6 +24,7 @@ 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;
@@ -34,10 +35,6 @@ 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;
@@ -45,8 +42,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;
@@ -211,15 +208,10 @@ 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.");
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);
Toml toml = new Toml().read(tree.readTextEntry(modToml));
ForgeNewModMetadata metadata = toml.to(ForgeNewModMetadata.class);
if (metadata == null || metadata.getMods().isEmpty())
throw new IOException("Mod " + modFile + " `%s` is malformed..".formatted(modToml.getName()));
throw new IOException("Mod " + modFile + " `mods.toml` is malformed..");
Mod mod = metadata.getMods().get(0);
ZipArchiveEntry manifestMF = tree.getEntry("META-INF/MANIFEST.MF");
String jarVersion = "";
@@ -232,7 +224,7 @@ public final class ForgeNewModMetadata {
}
}
ModLoaderType type = analyzeLoader(tomlParseResult, mod.getModId(), modLoaderType);
ModLoaderType type = analyzeLoader(toml, 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), "",
@@ -301,32 +293,23 @@ public final class ForgeNewModMetadata {
throw new IOException();
}
private static ModLoaderType analyzeLoader(TomlParseResult toml, String modID, ModLoaderType loader) {
List<Map<String, Object>> dependencies = null;
private static ModLoaderType analyzeLoader(Toml toml, String modID, ModLoaderType loader) {
List<HashMap<String, Object>> dependencies = null;
try {
TomlArray tomlArray = toml.getArray("dependencies." + modID);
if (tomlArray != null) {
dependencies = tomlArray.toList().stream().map( o -> ((TomlTable) o).toMap()).toList();
}
dependencies = toml.getList("dependencies." + modID);
} catch (ClassCastException ignored) { // https://github.com/HMCL-dev/HMCL/issues/5068
}
if (dependencies == null) {
try {
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();
}
dependencies = toml.getList("dependencies"); // ??? I have no idea why some of the Forge mods use [[dependencies]]
} catch (ClassCastException e) {
try {
TomlTable table = toml.getTable("dependencies");
Toml table = toml.getTable("dependencies");
if (table == null)
return loader;
TomlArray tomlArray = table.getArray(modID);
if (tomlArray != null) {
dependencies = tomlArray.toList().stream().map( o -> ((TomlTable) o).toMap()).toList();
}
dependencies = table.getList(modID);
} catch (Throwable ignored) {
}
}
@@ -338,7 +321,7 @@ public final class ForgeNewModMetadata {
ModLoaderType result = null;
loop:
for (Map<String, Object> dependency : dependencies) {
for (HashMap<String, Object> dependency : dependencies) {
switch ((String) dependency.get("modId")) {
case "forge":
result = ModLoaderType.FORGE;
@@ -24,7 +24,11 @@ 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.*;
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.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.io.NetworkUtils;
@@ -38,7 +42,6 @@ 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;
@@ -60,41 +63,6 @@ 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";
@@ -127,12 +95,6 @@ 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();
@@ -849,7 +811,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
author,
title,
description,
sortDisplayCategories(displayCategories),
displayCategories,
String.format("https://modrinth.com/%s/%s", projectType, projectId),
iconUrl,
this
@@ -1,97 +0,0 @@
/*
* 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,17 +209,6 @@ 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 {
@@ -17,14 +17,11 @@
*/
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;
@@ -35,12 +32,6 @@ 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);
}
@@ -57,10 +48,6 @@ 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
*
@@ -90,8 +77,9 @@ public final class CommandBuilder {
return this;
}
public CommandBuilder addWithoutParsing(String arg) {
raw.add(new Item(arg, false));
public CommandBuilder addWithoutParsing(String... args) {
for (String s : args)
raw.add(new Item(s, false));
return this;
}
@@ -101,26 +89,6 @@ 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);
}
@@ -130,6 +98,7 @@ 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('=');
@@ -137,16 +106,13 @@ public final class CommandBuilder {
addDefault(arg.substring(0, idx + 1), arg.substring(idx + 1), parse);
} else {
String opt = arg + "=";
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 + "'");
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;
}
continue;
}
raw.add(new Item(arg, parse));
}
@@ -185,9 +151,12 @@ public final class CommandBuilder {
}
}
if (allExistingArgs().noneMatch(arg::equals)) {
raw.add(new Item(arg, parse));
for (Item item : raw) {
if (item.arg.equals(arg)) {
continue loop;
}
}
raw.add(new Item(arg, parse));
}
}
@@ -196,12 +165,12 @@ public final class CommandBuilder {
}
private String addDefault(String opt, String value, boolean parse) {
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();
for (Item item : raw) {
if (item.arg.startsWith(opt)) {
LOG.info("Default option '" + opt + value + "' is suppressed by '" + item.arg + "'");
return item.arg;
}
}
raw.add(new Item(opt + value, parse));
return null;
}
@@ -220,15 +189,6 @@ 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 {
@@ -251,15 +211,6 @@ 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;
}
@@ -351,119 +302,6 @@ 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,28 +61,7 @@ public final class NativeUtils {
if (osVersion == null || osVersion.startsWith("5.") || osVersion.equals("6.0"))
return false;
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) {
}
}
}
// Currently we only need to use JNA on Windows
Native.getDefaultStringEncoding();
return true;
}
@@ -17,9 +17,6 @@
*/
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);
@@ -44,23 +41,8 @@ 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);
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 boolean isCompatibleWithX86Java() {
return Architecture.SYSTEM_ARCH.isX86() || SYSTEM_PLATFORM.equals(MACOS_ARM64) || SYSTEM_PLATFORM.equals(WINDOWS_ARM64);
}
public static Platform getPlatform(OperatingSystem os, Architecture arch) {
@@ -1,46 +0,0 @@
/*
* 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);
}
@@ -1,242 +0,0 @@
/*
* 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\""));
}
}
}
+1 -1
View File
@@ -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.14
versionRoot=3.13
+14 -30
View File
@@ -47,39 +47,23 @@ 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.11
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
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
Maintenance Phase: c3, after c2, 30d
```
In general, we release a new version branch every month,
+14 -30
View File
@@ -41,39 +41,23 @@ HMCL 具有两个主要发布通道:**稳定版通道**和**开发版通道**
## 发布模型
```mermaid
---
displayMode: compact
---
gantt
title HMCL 版本生命周期 (示例)
section 3.11
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
开发阶段: 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
维护阶段: c3, after c2, 30d
```
通常情况下,我们每个月发布一个版本分支,每个版本分支 `x.y` 生命周期为两个半月。
+14 -30
View File
@@ -41,39 +41,23 @@ HMCL 具有兩個主要發布管道:**穩定版管道**和**開發版管道**
## 發布模型
```mermaid
---
displayMode: compact
---
gantt
title HMCL 版本生命週期 (範例)
section 3.11
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
開發階段: 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
維護階段: c3, after c2, 30d
```
通常情況下,我們每個月發布一個版本分支,每個版本分支 `x.y` 生命週期為兩個半月。
+4 -4
View File
@@ -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"
tomlj = "1.1.1"
toml4j = "0.7.2"
xz = "1.12"
lz4 = "1.10.4.1"
fx-gson = "5.0.0"
@@ -13,6 +13,7 @@ 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"
@@ -22,7 +23,6 @@ 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" }
tomlj = { module = "org.tomlj:tomlj", version.ref = "tomlj" }
toml = { module = "com.moandjiezana.toml:toml4j", version.ref = "toml4j" }
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,6 +47,7 @@ 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" }
@@ -56,7 +57,6 @@ 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" }