Compare commits

..
8 Commits
Author SHA1 Message Date
GlavoandBurning_TNT 997b75ba19 修复无法为 MC 26.1.1 从官方源安装 NeoForge 的问题 (#5926)
https://github.com/HMCL-dev/HMCL/pull/5910

Co-authored-by: Burning_TNT <pangyl08@163.com>
2026-04-06 21:15:07 +08:00
Glavo 373e5c1ff0 [release/3.12] 支持 26w14a (#5908)
https://github.com/HMCL-dev/HMCL/pull/5892
2026-04-04 20:23:14 +08:00
Glavoand3gf8jv4dv d095eab8fc [release/3.12] 通过 Java Agent 优化 LWJGL 3.4.1 性能 (#5886)
https://github.com/HMCL-dev/HMCL/pull/5870
https://github.com/HMCL-dev/HMCL/pull/5873
https://github.com/HMCL-dev/HMCL/pull/5874
https://github.com/HMCL-dev/HMCL/pull/5882

---------

Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2026-03-31 21:25:08 +08:00
Glavoandneveler 447edff1ed [release/3.12] 种子地图、要塞地图、下届要塞地图和末地城地图支持 26.1 (#5887)
https://github.com/HMCL-dev/HMCL/pull/5860

Co-authored-by: neveler <55753029+neveler@users.noreply.github.com>
2026-03-31 21:12:44 +08:00
Glavo f7024a4c0f [release/3.12] 更新文言文翻译 (#5849)
https://github.com/HMCL-dev/HMCL/pull/5845
2026-03-24 21:44:35 +08:00
Glavo 31c7e3a60f [release/3.12] Update default Java version to JAVA_25 (#5846)
https://github.com/HMCL-dev/HMCL/pull/5841
2026-03-24 21:38:17 +08:00
Glavo 43cfe29eeb [release/3.12] 修复 JFXTextField 导致 GPU 占用率异常高的问题 (#5847)
https://github.com/HMCL-dev/HMCL/pull/5839
2026-03-24 21:37:49 +08:00
Glavo 4921804283 [release/3.12] Add Minecraft 26.1 snapshot/pre/rc versions (#5848)
https://github.com/HMCL-dev/HMCL/pull/5833
2026-03-24 21:37:20 +08:00
115 changed files with 1153 additions and 2231 deletions

No files matched your search

-1
View File
@@ -57,7 +57,6 @@ dependencies {
implementation(project(":HMCLBoot"))
implementation("libs:JFoenix")
implementation(libs.twelvemonkeys.imageio.webp)
implementation(libs.fxsvgimage)
implementation(libs.java.info)
implementation(libs.monet.fx)
implementation(libs.nayuki.qrcodegen)
@@ -23,13 +23,21 @@ import com.jfoenix.controls.events.JFXDialogEvent;
import com.jfoenix.converters.DialogTransitionConverter;
import com.jfoenix.effects.JFXDepthManager;
import com.jfoenix.transitions.CachedTransition;
import javafx.animation.*;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.animation.Transition;
import javafx.beans.DefaultProperty;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.css.*;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableObjectProperty;
import javafx.css.Styleable;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleableProperty;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
@@ -41,6 +49,7 @@ import javafx.scene.image.WritableImage;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
@@ -146,6 +155,7 @@ public class JFXDialog extends StackPane {
});
contentHolder = new StackPane();
contentHolder.setBackground(new Background(new BackgroundFill(Color.WHITE, new CornerRadii(2), null)));
JFXDepthManager.setDepth(contentHolder, 4);
contentHolder.setPickOnBounds(false);
// ensure stackpane is never resized beyond it's preferred size
@@ -371,14 +381,6 @@ public class JFXDialog extends StackPane {
setCycleDuration(Duration.seconds(0.4));
setDelay(Duration.ZERO);
}
@Override
protected void starting() {
}
@Override
protected void stopping() {
}
}
/***************************************************************************
@@ -40,16 +40,12 @@ import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.*;
import javafx.util.Duration;
import org.glavo.monetfx.ColorScheme;
import org.jackhuang.hmcl.setting.StyleSheets;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
* @author Shadi Shaheen
*/
@@ -82,7 +78,7 @@ public class JFXCustomColorPickerDialog extends StackPane {
// create JFX Decorator
pickerDecorator = new JFXDecorator(dialog, this, false, false, false);
pickerDecorator.setOnCloseButtonAction(this::close);
pickerDecorator.setOnCloseButtonAction(this::updateColor);
pickerDecorator.setPickOnBounds(false);
customScene = new Scene(pickerDecorator, Color.TRANSPARENT);
StyleSheets.init(customScene);
@@ -157,22 +153,6 @@ public class JFXCustomColorPickerDialog extends StackPane {
paraTransition.play();
});
container.getChildren().add(tabs);
HBox actionsHBox = new HBox();
actionsHBox.getStyleClass().add("jfx-color-dialog-actions");
JFXButton acceptButton = new JFXButton(i18n("button.ok"));
acceptButton.setOnAction(event -> updateColor());
acceptButton.getStyleClass().add("jfx-color-dialog-accept");
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.setOnAction(event -> close());
cancelButton.getStyleClass().add("jfx-color-dialog-cancel");
actionsHBox.getChildren().addAll(acceptButton, cancelButton);
container.getChildren().add(actionsHBox);
initRun = () -> {
// change tabs labels font color according to the selected color
pane.backgroundProperty().addListener((o, oldVal, newVal) -> {
@@ -199,7 +179,7 @@ public class JFXCustomColorPickerDialog extends StackPane {
(int) (newColor.getRed() * 255),
(int) (newColor.getGreen() * 255),
(int) (newColor.getBlue() * 255));
String rgb = String.format("rgb(%d, %d, %d)",
String rgb = String.format("rgba(%d, %d, %d, 1)",
(int) (newColor.getRed() * 255),
(int) (newColor.getGreen() * 255),
(int) (newColor.getBlue() * 255));
@@ -252,13 +232,7 @@ public class JFXCustomColorPickerDialog extends StackPane {
hexField.focusColorProperty().bind(Bindings.createObjectBinding(() -> {
return pane.getBackground().getFills().get(0).getFill();
}, pane.backgroundProperty()));
acceptButton.textFillProperty().bind(Bindings.createObjectBinding(() -> {
Color fill = (Color) pane.getBackground().getFills().get(0).getFill();
return ColorScheme.newBuilder(Themes.getColorScheme())
.setPrimaryColorSeed(fill)
.build()
.getPrimary();
}, pane.backgroundProperty()));
((Pane) pickerDecorator.lookup(".jfx-decorator-buttons-container")).backgroundProperty()
.bind(Bindings.createObjectBinding(() -> {
@@ -288,6 +262,9 @@ public class JFXCustomColorPickerDialog extends StackPane {
});
};
container.getChildren().add(tabs);
this.getChildren().add(container);
this.setPadding(new Insets(0));
@@ -317,11 +294,7 @@ public class JFXCustomColorPickerDialog extends StackPane {
if (!systemChange) {
userChange = true;
try {
Color color = Color.valueOf(colorWebString);
if (color.getOpacity() != 1.0) {
color = Color.color(color.getRed(), color.getGreen(), color.getBlue());
}
curvedColorPicker.setColor(color);
curvedColorPicker.setColor(Color.valueOf(colorWebString));
} catch (IllegalArgumentException ignored) {
// if color is not valid then do nothing
}
@@ -25,7 +25,6 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import javax.swing.JOptionPane;
import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
@@ -50,8 +49,6 @@ public final class EntryPoint {
createHMCLDirectories();
LOG.start(Metadata.HMCL_CURRENT_DIRECTORY.resolve("logs"));
checkWine();
setupJavaFXVMOptions();
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
@@ -223,20 +220,6 @@ public final class EntryPoint {
}
}
private static void checkWine() {
if (OperatingSystem.isRunningUnderWine()) {
SwingUtils.initLookAndFeel();
LOG.warning("HMCL is running under Wine or its distributions!");
int result = JOptionPane.showOptionDialog(null, i18n("fatal.wine_warning"), i18n("message.warning"), JOptionPane.OK_CANCEL_OPTION,
JOptionPane.WARNING_MESSAGE, null, null, null);
if (result == JOptionPane.CANCEL_OPTION || result == JOptionPane.CLOSED_OPTION) {
exit(1);
}
}
}
private static void addEnableNativeAccess() {
if (JavaRuntime.CURRENT_VERSION > 21) {
try {
@@ -17,36 +17,35 @@
*/
package org.jackhuang.hmcl;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ObservableBooleanValue;
import javafx.geometry.Rectangle2D;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.input.Clipboard;
import javafx.scene.input.DataFormat;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.setting.SambaException;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.FileSaver;
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.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.CrashReporter;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.NativeUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemInfo;
import java.io.File;
import java.io.IOException;
@@ -66,8 +65,8 @@ import java.util.concurrent.TimeUnit;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
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 Launcher extends Application {
public static final CookieManager COOKIE_MANAGER = new CookieManager();
@@ -112,7 +111,7 @@ public final class Launcher extends Application {
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
&& ConfigHolder.isNewlyCreated()
&& System.getProperty("user.dir").startsWith("/private/var/folders/")) {
if (!confirmWithCountdown(AlertType.WARNING, i18n("fatal.mac_app_translocation"), 5))
if (showAlert(AlertType.WARNING, i18n("fatal.mac_app_translocation"), ButtonType.YES, ButtonType.NO) == ButtonType.NO)
return;
} else {
checkConfigInTempDir();
@@ -183,27 +182,6 @@ public final class Launcher extends Application {
return new Alert(alertType, contentText, buttons).showAndWait().orElse(null);
}
private static boolean confirmWithCountdown(Alert.AlertType alertType, String contentText, int seconds) {
Alert alert = new Alert(alertType, contentText, ButtonType.YES, ButtonType.NO);
Button okButton = (Button) alert.getDialogPane().lookupButton(ButtonType.YES);
okButton.setDisable(true);
KeyFrame[] keyFrames = new KeyFrame[seconds + 1];
for (int i = 0; i < seconds; i++) {
keyFrames[i] = new KeyFrame(Duration.seconds(i),
new KeyValue(okButton.textProperty(), i18n("button.ok.countdown", seconds - i)));
}
keyFrames[seconds] = new KeyFrame(Duration.seconds(seconds),
new KeyValue(okButton.textProperty(), i18n("button.ok")),
new KeyValue(okButton.disableProperty(), false));
Timeline timeline = new Timeline(keyFrames);
alert.setOnShown(e -> timeline.play());
alert.setOnCloseRequest(e -> timeline.stop());
return alert.showAndWait().orElse(null) == ButtonType.YES;
}
private static boolean isConfigInTempDir() {
String configPath = ConfigHolder.configLocation().toString();
@@ -243,7 +221,7 @@ public final class Launcher extends Application {
private static void checkConfigInTempDir() {
if (ConfigHolder.isNewlyCreated() && isConfigInTempDir()
&& !confirmWithCountdown(AlertType.WARNING, i18n("fatal.config_in_temp_dir"), 5)) {
&& showAlert(AlertType.WARNING, i18n("fatal.config_in_temp_dir"), ButtonType.YES, ButtonType.NO) == ButtonType.NO) {
EntryPoint.exit(0);
}
}
@@ -49,7 +49,6 @@ public final class Metadata {
public static final String ABOUT_URL = PUBLISH_URL + "/about";
public static final String DOWNLOAD_URL = PUBLISH_URL + "/download";
public static final String HMCL_UPDATE_URL = System.getProperty("hmcl.update_source.override", PUBLISH_URL + "/api/update_link");
public static final String MANUAL_UPDATE_URL = "https://github.com/HMCL-dev/HMCL/releases";
public static final String DOCS_URL = "https://docs.hmcl.net";
public static final String CONTACT_URL = DOCS_URL + "/help.html";
@@ -1,20 +1,3 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.countly;
import org.jackhuang.hmcl.Metadata;
@@ -310,7 +310,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
Optional<Path> iconFile = getVersionIconFile(id);
if (iconFile.isPresent()) {
try {
return FXUtils.loadImage(iconFile.get(), 64, 64, true, true);
return FXUtils.loadImage(iconFile.get());
} catch (Exception e) {
LOG.warning("Failed to load version icon of " + id, e);
}
@@ -567,21 +567,6 @@ public final class Config extends ObservableSetting {
this.versionListSource.set(versionListSource);
}
@SerializedName("defaultAddonSource")
private final StringProperty defaultAddonSource = new SimpleStringProperty("modrinth");
public StringProperty defaultAddonSourceProperty() {
return defaultAddonSource;
}
public String getDefaultAddonSource() {
return defaultAddonSource.get();
}
public void setDefaultAddonSource(String defaultAddonSource) {
this.defaultAddonSource.set(defaultAddonSource);
}
@SerializedName("hasProxy")
private final BooleanProperty hasProxy = new SimpleBooleanProperty();
@@ -1,20 +1,3 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.setting;
public final class SambaException extends RuntimeException {
@@ -235,8 +235,8 @@ public final class FXUtils {
public static final String DEFAULT_MONOSPACE_FONT = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "Consolas" : "Monospace";
public static final List<String> IMAGE_EXTENSIONS = List.of(
"png", "jpg", "jpeg", "bmp", "gif", "webp", "svg", "apng"
public static final List<String> IMAGE_EXTENSIONS = Lang.immutableListOf(
"png", "jpg", "jpeg", "bmp", "gif", "webp", "apng"
);
private static final Map<String, Image> builtinImageCache = new ConcurrentHashMap<>();
@@ -42,7 +42,6 @@ import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
@@ -274,10 +273,10 @@ public class GameCrashWindow extends Stage {
logWindow.show();
}
private CompletableFuture<Path> exportGameCrashInfo() {
private void exportGameCrashInfo() {
Path logFile = Paths.get("minecraft-exported-crash-info-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".zip").toAbsolutePath();
return CompletableFuture.supplyAsync(() ->
CompletableFuture.supplyAsync(() ->
logs.stream().map(Log::getLog).collect(Collectors.joining("\n")))
.thenComposeAsync(logs -> {
long processStartTime = managedProcess.getProcess().info()
@@ -302,7 +301,20 @@ public class GameCrashWindow extends Stage {
return false;
}
});
}).thenApply(ignored -> logFile);
})
.handleAsync((result, exception) -> {
if (exception == null) {
FXUtils.showFileInExplorer(logFile);
var dialog = new MessageDialogPane.Builder(i18n("settings.launcher.launcher_log.export.success", logFile), i18n("message.success"), MessageDialogPane.MessageType.SUCCESS).ok(null).build();
DialogUtils.show(stackPane, dialog);
} else {
LOG.warning("Failed to export game crash info", exception);
var dialog = new MessageDialogPane.Builder(i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception), i18n("message.error"), MessageDialogPane.MessageType.ERROR).ok(null).build();
DialogUtils.show(stackPane, dialog);
}
return null;
}, Schedulers.javafx());
}
private final class View extends VBox {
@@ -432,35 +444,8 @@ public class GameCrashWindow extends Stage {
HBox toolBar = new HBox();
VBox.setMargin(toolBar, new Insets(0, 0, 4, 0));
{
SpinnerPane exportButtonPane = new SpinnerPane();
exportButtonPane.getStyleClass().add("small-spinner-pane");
JFXButton exportButton = FXUtils.newRaisedButton(i18n("logwindow.export_game_crash_logs"));
exportButtonPane.setContent(exportButton);
exportButton.setOnAction(e -> {
exportButtonPane.showSpinner();
exportGameCrashInfo().whenCompleteAsync((result, exception) -> {
exportButtonPane.hideSpinner();
if (exception == null) {
FXUtils.showFileInExplorer(result);
var dialog = new MessageDialogPane.Builder(
i18n("settings.launcher.launcher_log.export.success", result),
i18n("message.success"),
MessageDialogPane.MessageType.SUCCESS
).ok(null).build();
DialogUtils.show(stackPane, dialog);
} else {
LOG.warning("Failed to export game crash info", exception);
var dialog = new MessageDialogPane.Builder(
i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception),
i18n("message.error"),
MessageDialogPane.MessageType.ERROR
).ok(null).build();
DialogUtils.show(stackPane, dialog);
}
}, Schedulers.javafx());
});
JFXButton exportGameCrashInfoButton = FXUtils.newRaisedButton(i18n("logwindow.export_game_crash_logs"));
exportGameCrashInfoButton.setOnAction(e -> exportGameCrashInfo());
JFXButton logButton = FXUtils.newRaisedButton(i18n("logwindow.title"));
logButton.setOnAction(e -> showLogWindow());
@@ -472,7 +457,7 @@ public class GameCrashWindow extends Stage {
toolBar.setPadding(new Insets(8));
toolBar.setSpacing(8);
toolBar.getStyleClass().add("jfx-tool-bar");
toolBar.getChildren().setAll(exportButtonPane, logButton, helpButton);
toolBar.getChildren().setAll(exportGameCrashInfoButton, logButton, helpButton);
}
getChildren().setAll(titlePane, infoPane, moddedPane, gameDirPane, toolBar);
@@ -36,7 +36,6 @@ import javafx.stage.Stage;
import org.jackhuang.hmcl.game.GameDumpGenerator;
import org.jackhuang.hmcl.game.Log;
import org.jackhuang.hmcl.setting.StyleSheets;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
@@ -242,10 +241,6 @@ public final class LogWindow extends Stage {
});
}
private ManagedProcess getGameProcess() {
return gameProcess;
}
@Override
protected Skin<?> createDefaultSkin() {
return new LogWindowSkin(this);
@@ -430,13 +425,6 @@ public final class LogWindow extends Stage {
clearButton.setOnAction(e -> getSkinnable().onClear());
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, exportDumpPane, clearButton);
control.getGameProcess().getProcess()
.onExit()
.thenRunAsync(() -> {
terminateButton.setDisable(true);
exportDumpButton.setDisable(true);
}, Schedulers.javafx());
vbox.getChildren().add(bottom);
}
}
@@ -1,228 +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 javafx.application.Platform;
import javafx.beans.property.*;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import org.jackhuang.hmcl.util.FXThread;
import org.jackhuang.hmcl.util.MathUtils;
import org.jackhuang.hmcl.util.platform.SystemInfo;
import org.jackhuang.hmcl.util.platform.hardware.PhysicalMemoryStatus;
import java.lang.ref.WeakReference;
/// @author Glavo
public final class MemoryStatusBar extends Control {
private static final String DEFAULT_STYLE_CLASS = "memory-status-bar";
public MemoryStatusBar() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
setPrefWidth(200);
}
private final ReadOnlyObjectProperty<PhysicalMemoryStatus> memoryStatus = UpdateMemoryStatus.memoryStatusProperty();
/// The memory status of the system.
///
/// The property will be automatically updated.
///
/// The property has no bean, because it may share between multiple MemoryStatusBar instances.
public ReadOnlyObjectProperty<PhysicalMemoryStatus> memoryStatusProperty() {
return memoryStatus;
}
public PhysicalMemoryStatus getMemoryStatus() {
return memoryStatus.get();
}
private final DoubleProperty memoryAllocated = new SimpleDoubleProperty();
public DoubleProperty memoryAllocatedProperty() {
return memoryAllocated;
}
public double getMemoryAllocated() {
return memoryAllocated.get();
}
public void setMemoryAllocated(double memoryAllocated) {
this.memoryAllocated.set(memoryAllocated);
}
@Override
protected SkinBase<MemoryStatusBar> createDefaultSkin() {
return new Skin(this);
}
private static final class Skin extends SkinBase<MemoryStatusBar> {
private static final int HEIGHT = 4;
private final StackPane track;
private final Rectangle memoryUsed;
private final Rectangle memoryAllocate;
private double contentWidth = -1;
Skin(MemoryStatusBar control) {
super(control);
track = new StackPane();
track.getStyleClass().add("track");
track.setPadding(new Insets(HEIGHT, 0, 0, 0));
memoryUsed = new Rectangle();
memoryUsed.setManaged(false);
memoryUsed.setArcWidth(HEIGHT);
memoryUsed.setArcHeight(HEIGHT);
memoryUsed.getStyleClass().add("memory-used");
memoryAllocate = new Rectangle();
memoryAllocate.setManaged(false);
memoryAllocate.setArcWidth(HEIGHT);
memoryAllocate.setArcHeight(HEIGHT);
memoryAllocate.getStyleClass().add("memory-allocate");
this.getChildren().setAll(track, memoryAllocate, memoryUsed);
registerInvalidationListener(control.memoryStatusProperty(), it -> updateBar());
registerInvalidationListener(control.memoryAllocatedProperty(), it -> updateBar());
}
private void updateBar() {
if (contentWidth <= 0) {
// not yet layout
return;
}
PhysicalMemoryStatus status = getSkinnable().getMemoryStatus();
double total = status.getTotal();
double used = status.getUsed();
double allocated = getSkinnable().getMemoryAllocated();
if (total <= 0) {
memoryUsed.setWidth(contentWidth);
memoryAllocate.setWidth(contentWidth);
return;
}
double usedWidth = MathUtils.clamp(contentWidth * (used / total), 0, contentWidth);
double allocatedWidth = MathUtils.clamp(contentWidth * (allocated / total) + usedWidth, 0, contentWidth);
memoryUsed.setWidth(usedWidth);
memoryAllocate.setWidth(allocatedWidth);
}
@Override
protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
track.resizeRelocate(contentX, contentY, contentWidth, contentHeight);
memoryUsed.relocate(contentX, contentY);
memoryAllocate.relocate(contentX, contentY);
this.contentWidth = contentWidth;
memoryUsed.setHeight(contentHeight);
memoryAllocate.setHeight(contentHeight);
updateBar();
}
@Override
public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) {
return Node.BASELINE_OFFSET_SAME_AS_HEIGHT;
}
@Override
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
return leftInset + track.prefWidth(height) + rightInset;
}
@Override
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
return topInset + HEIGHT + bottomInset;
}
@Override
protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
return computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
}
}
private static final class UpdateMemoryStatus extends Thread {
private static final int UPDATE_INTERVAL = 3000;
@FXThread
private static WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyCache;
@FXThread
static ReadOnlyObjectProperty<PhysicalMemoryStatus> memoryStatusProperty() {
if (memoryStatusPropertyCache != null) {
var property = memoryStatusPropertyCache.get();
if (property != null) {
return property;
}
}
ObjectProperty<PhysicalMemoryStatus> property = new SimpleObjectProperty<>(PhysicalMemoryStatus.INVALID);
memoryStatusPropertyCache = new WeakReference<>(property);
new UpdateMemoryStatus(memoryStatusPropertyCache).start();
return property;
}
private final WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyRef;
UpdateMemoryStatus(WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyRef) {
this.memoryStatusPropertyRef = memoryStatusPropertyRef;
setName("UpdateMemoryStatus");
setDaemon(true);
setPriority(Thread.MIN_PRIORITY);
}
@Override
public void run() {
while (true) {
PhysicalMemoryStatus status = SystemInfo.getPhysicalMemoryStatus();
var memoryStatusProperty = memoryStatusPropertyRef.get();
if (memoryStatusProperty == null)
return;
if (Controllers.isStopped())
return;
Platform.runLater(() -> memoryStatusProperty.set(status));
try {
//noinspection BusyWait
Thread.sleep(UPDATE_INTERVAL);
} catch (InterruptedException e) {
return;
}
}
}
}
}
@@ -26,7 +26,7 @@ import javafx.scene.control.ScrollBar;
import javafx.scene.control.Skin;
import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle;
import org.jackhuang.hmcl.util.MathUtils;
import org.jackhuang.hmcl.util.Lang;
// Referenced in root.css
@SuppressWarnings("unused")
@@ -75,7 +75,7 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
*/
if (getSkinnable().getMax() > getSkinnable().getMin()) {
dragStart = thumb.localToParent(me.getX(), me.getY());
double clampedValue = MathUtils.clamp(getSkinnable().getValue(), getSkinnable().getMin(), getSkinnable().getMax());
double clampedValue = Lang.clamp(getSkinnable().getMin(), getSkinnable().getValue(), getSkinnable().getMax());
preDragThumbPos = (clampedValue - getSkinnable().getMin()) / (getSkinnable().getMax() - getSkinnable().getMin());
me.consume();
}
@@ -107,7 +107,7 @@ public class FloatScrollBarSkin implements Skin<ScrollBar> {
getSkinnable().requestFocus();
double newValue = (position * (getSkinnable().getMax() - getSkinnable().getMin())) + getSkinnable().getMin();
if (!Double.isNaN(newValue)) {
getSkinnable().setValue(MathUtils.clamp(newValue, getSkinnable().getMin(), getSkinnable().getMax()));
getSkinnable().setValue(Lang.clamp(getSkinnable().getMin(), newValue, getSkinnable().getMax()));
}
}
@@ -30,6 +30,9 @@ import org.jackhuang.hmcl.ui.SVG;
public class LineButton extends LineButtonBase {
private static final String DEFAULT_STYLE_CLASS = "line-button";
private static final int IDX_TRAILING_TEXT = IDX_TRAILING;
private static final int IDX_TRAILING_ICON = IDX_TRAILING + 1;
public static LineButton createNavigationButton() {
var button = new LineButton();
button.setTrailingIcon(SVG.ARROW_FORWARD);
@@ -47,14 +50,7 @@ public class LineButton extends LineButtonBase {
public LineButton() {
getStyleClass().add(DEFAULT_STYLE_CLASS);
}
protected int getTrailingTextIndex() {
return IDX_TRAILING;
}
protected int getTrailingIconIndex() {
return getTrailingTextIndex() + 1;
container.setMouseTransparent(true);
}
private ObjectProperty<EventHandler<ActionEvent>> onAction;
@@ -114,13 +110,12 @@ public class LineButton extends LineButtonBase {
trailingTextLabel = new Label();
trailingTextLabel.getStyleClass().add("trailing-label");
trailingTextLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
trailingTextLabel.setMouseTransparent(true);
}
trailingTextLabel.setText(message);
setNode(getTrailingTextIndex(), trailingTextLabel);
setNode(IDX_TRAILING_TEXT, trailingTextLabel);
} else if (trailingTextLabel != null) {
trailingTextLabel.setText("");
setNode(getTrailingTextIndex(), null);
setNode(IDX_TRAILING_TEXT, null);
}
}
};
@@ -154,7 +149,7 @@ public class LineButton extends LineButtonBase {
@Override
protected void invalidated() {
setNode(getTrailingIconIndex(), get());
setNode(IDX_TRAILING_ICON, get());
}
};
@@ -176,7 +171,6 @@ public class LineButton extends LineButtonBase {
public void setTrailingIcon(SVG rightIcon, double size) {
Node rightIconNode = rightIcon.createIcon(size);
rightIconNode.getStyleClass().add("trailing-icon");
rightIconNode.setMouseTransparent(true);
setTrailingIcon(rightIconNode);
}
}
@@ -64,7 +64,6 @@ public abstract class LineComponent extends StackPane implements NoPaddingCompon
this.container = new HBox(SPACING);
container.getStyleClass().add("line-component-container");
container.setAlignment(Pos.CENTER_LEFT);
container.setPickOnBounds(true);
this.titleLabel = new Label();
titleLabel.getStyleClass().add("title-label");
@@ -43,7 +43,7 @@ import java.util.function.Function;
import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition;
/// @author Glavo
public class LineSelectButton<T> extends LineButton {
public final class LineSelectButton<T> extends LineButton {
private static final String DEFAULT_STYLE_CLASS = "line-select-button";
private static final PseudoClass SELECTED_PSEUDO_CLASS = PseudoClass.getPseudoClass("selected");
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui.construct;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.StringProperty;
import javafx.beans.property.StringPropertyBase;
@@ -169,13 +168,11 @@ public class TwoLineListItem extends VBox {
var scrollPane = new ScrollPane(tagsBox);
HBox.setHgrow(scrollPane, Priority.ALWAYS);
lblTitle.setMinWidth(Label.USE_PREF_SIZE);
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
FXUtils.onChangeAndOperate(tagsBox.heightProperty(), height -> FXUtils.setLimitHeight(scrollPane, height.doubleValue()));
scrollPane.setPrefWidth(50);
firstLine.getChildren().setAll(lblTitle, scrollPane);
tags.addListener((InvalidationListener) ignored -> scrollPane.requestLayout());
}
return tags;
}
@@ -50,7 +50,7 @@ import org.jackhuang.hmcl.ui.construct.JFXDialogPane;
import org.jackhuang.hmcl.ui.construct.Navigator;
import org.jackhuang.hmcl.ui.wizard.Refreshable;
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.MathUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;
@@ -218,7 +218,7 @@ public class DecoratorController {
return new Background(new BackgroundFill(new Color(1, 1, 1, 0.5), CornerRadii.EMPTY, Insets.EMPTY));
case PAINT:
Paint paint = config().getBackgroundPaint();
double opacity = MathUtils.clamp(config().getBackgroundImageOpacity(), 0, 100) / 100.;
double opacity = Lang.clamp(0, config().getBackgroundImageOpacity(), 100) / 100.;
if (paint instanceof Color || paint == null) {
Color color = (Color) paint;
if (color == null)
@@ -1,20 +1,3 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.download;
import com.jfoenix.controls.JFXButton;
@@ -19,17 +19,9 @@ 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;
import javafx.scene.image.Image;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.WritableImage;
import javafx.scene.paint.Color;
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.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.image.apng.Png;
import org.jackhuang.hmcl.ui.image.apng.argb8888.Argb8888Bitmap;
import org.jackhuang.hmcl.ui.image.apng.argb8888.Argb8888BitmapSequence;
@@ -47,9 +39,7 @@ import javax.imageio.stream.ImageInputStream;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Pattern;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -83,46 +73,6 @@ public final class ImageUtils {
return SwingFXUtils.toFXImage(bufferedImage, requestedWidth, requestedHeight, preserveRatio, smooth);
};
public static final ImageLoader SVG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
String content = new String(input.readAllBytes(), StandardCharsets.UTF_8);
LoaderParameters parameters = new LoaderParameters();
parameters.autoStartAnimations = false;
SVGImage image;
if (Platform.isFxApplicationThread()) {
image = SVGLoader.load(content, parameters);
} else {
// TODO: Currently, SVGLoader.load(...) requires the javafx.swing module if it operates on a non-JavaFX thread.
image = CompletableFuture.supplyAsync(
() -> SVGLoader.load(content, parameters),
Schedulers.javafx()
).get();
}
if (image == null)
throw new IOException("Failed to load SVG image");
var snapshotParameters = new SnapshotParameters();
snapshotParameters.setFill(Color.TRANSPARENT);
if (requestedWidth <= 0. || requestedHeight <= 0.) {
return image.toImage(snapshotParameters);
}
double scaleX = requestedWidth / image.getScaledWidth();
double scaleY = requestedHeight / image.getScaledHeight();
if (preserveRatio || scaleX == scaleY) {
double scale = Math.min(scaleX, scaleY);
return image.scale(scale).toImage(snapshotParameters);
} else {
// FIXME: Use DEFAULT_SVG_SNAPSHOT_PARAMS
return image.toImageScaled(ScaleQuality.RENDER_QUALITY, scaleX, scaleY);
}
};
public static final ImageLoader APNG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
if (!"true".equals(System.getProperty("hmcl.experimental.apng", "true")))
return DEFAULT.load(input, requestedWidth, requestedHeight, preserveRatio, smooth);
@@ -186,13 +136,11 @@ public final class ImageUtils {
public static final Map<String, ImageLoader> EXT_TO_LOADER = Map.of(
"webp", WEBP,
"svg", SVG,
"apng", APNG
);
public static final Map<String, ImageLoader> CONTENT_TYPE_TO_LOADER = Map.of(
"image/webp", WEBP,
"image/svg+xml", SVG,
"image/apng", APNG
);
@@ -217,14 +165,6 @@ public final class ImageUtils {
&& Arrays.equals(headerBuffer, 8, 12, WEBP_HEADER, 0, 4);
}
private static final byte[] SVG_HEADER = "<svg".getBytes(StandardCharsets.US_ASCII);
// This is currently a simple check, more complex checks can be considered in the future
public static boolean isSVG(byte[] headerBuffer) {
return headerBuffer.length > SVG_HEADER.length
&& Arrays.equals(headerBuffer, 0, SVG_HEADER.length, SVG_HEADER, 0, SVG_HEADER.length);
}
private static final byte[] PNG_HEADER = {
(byte) 0x89, (byte) 0x50, (byte) 0x4e, (byte) 0x47,
(byte) 0x0d, (byte) 0x0a, (byte) 0x1a, (byte) 0x0a,
@@ -292,8 +232,6 @@ public final class ImageUtils {
return WEBP;
if (isApng(headerBuffer))
return APNG;
if (isSVG(headerBuffer))
return SVG;
return null;
}
@@ -18,8 +18,10 @@
package org.jackhuang.hmcl.ui.main;
import com.google.gson.*;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.theme.Themes;
@@ -28,7 +30,6 @@ import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.LineButton;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.io.IOException;
@@ -37,18 +38,11 @@ import java.io.InputStream;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class AboutPage extends SpinnerPane {
public final class AboutPage extends StackPane {
private final WeakListenerHolder holder = new WeakListenerHolder();
public AboutPage() {
VBox content = new VBox();
content.getStyleClass().add("spinner-pane-content");
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setFitToWidth(true);
FXUtils.smoothScrolling(scrollPane);
setContent(scrollPane);
ComponentList about = new ComponentList();
{
var launcher = LineButton.createExternalLinkButton(Metadata.PUBLISH_URL);
@@ -90,16 +84,27 @@ public final class AboutPage extends SpinnerPane {
legal.getContent().setAll(copyright, claim, openSource);
}
VBox content = new VBox(16);
content.setPadding(new Insets(10));
content.getChildren().setAll(
ComponentList.createComponentListTitle(i18n("about")),
about,
ComponentList.createComponentListTitle(i18n("about.thanks_to")),
thanks,
ComponentList.createComponentListTitle(i18n("about.dependency")),
deps,
ComponentList.createComponentListTitle(i18n("about.legal")),
legal
);
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setFitToWidth(true);
FXUtils.smoothScrolling(scrollPane);
getChildren().setAll(scrollPane);
}
private static Image loadImage(String url) {
@@ -85,13 +85,7 @@ public class DownloadSettingsPage extends StackPane {
downloadSourcePane.setItems(DownloadProviders.DIRECT_PROVIDERS.keySet());
downloadSourcePane.valueProperty().bindBidirectional(config().downloadTypeProperty());
var defaultAddonSourcePane = new LineSelectButton<String>();
defaultAddonSourcePane.setTitle(i18n("settings.launcher.default_addon_source"));
defaultAddonSourcePane.setConverter(key -> I18n.i18n("mods." + key));
defaultAddonSourcePane.setItems("modrinth", "curseforge");
defaultAddonSourcePane.valueProperty().bindBidirectional(config().defaultAddonSourceProperty());
downloadSource.getContent().setAll(autoChooseDownloadSource, versionListSourcePane, downloadSourcePane, defaultAddonSourcePane);
downloadSource.getContent().setAll(autoChooseDownloadSource, versionListSourcePane, downloadSourcePane);
}
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.download_source")), downloadSource);
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.ui.main;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.theme.Themes;
@@ -36,7 +37,9 @@ public class FeedbackPage extends SpinnerPane {
public FeedbackPage() {
VBox content = new VBox();
content.getStyleClass().add("spinner-pane-content");
content.setPadding(new Insets(10));
content.setSpacing(10);
content.setFillWidth(true);
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setFitToWidth(true);
FXUtils.smoothScrolling(scrollPane);
@@ -81,5 +84,8 @@ public class FeedbackPage extends SpinnerPane {
ComponentList.createComponentListTitle(i18n("contact.feedback")),
feedback
);
this.setContent(content);
}
}
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.ui.main;
import com.google.gson.annotations.SerializedName;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Metadata;
@@ -41,7 +42,9 @@ public class HelpPage extends SpinnerPane {
public HelpPage() {
content = new VBox();
content.getStyleClass().add("spinner-pane-content");
content.setPadding(new Insets(10));
content.setSpacing(10);
content.setFillWidth(true);
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setFitToWidth(true);
FXUtils.smoothScrolling(scrollPane);
@@ -18,24 +18,24 @@
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXRadioButton;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.StringProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Cursor;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.*;
import javafx.scene.text.TextAlignment;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.setting.EnumCommonDirectory;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
@@ -46,7 +46,6 @@ import org.jackhuang.hmcl.upgrade.UpdateChannel;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.AprilFools;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
@@ -62,16 +61,20 @@ import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class SettingsPage extends ScrollPane {
@SuppressWarnings("FieldCanBeLocal")
private final ToggleGroup updateChannelGroup;
@SuppressWarnings("FieldCanBeLocal")
private final InvalidationListener updateListener;
@@ -118,59 +121,82 @@ public final class SettingsPage extends ScrollPane {
settingsPane.getContent().add(sponsorPane);
}
ObjectProperty<UpdateChannel> updateChannel;
{
JFXButton updateButton = new JFXButton();
updateButton.setGraphic(SVG.UPDATE.createIcon(20));
updateButton.setOnAction(e -> onUpdate());
updateButton.setPadding(Insets.EMPTY);
FXUtils.installFastTooltip(updateButton, i18n("update.tooltip"));
var updatePane = new LineSelectButton<UpdateChannel>() {
{
getStyleClass().add("update-pane");
setNode(IDX_TRAILING, updateButton);
}
@Override
protected int getTrailingTextIndex() {
return LineComponent.IDX_TRAILING + 1;
}
};
updateChannel = updatePane.valueProperty();
ComponentSublist updatePane = new ComponentSublist();
updatePane.setTitle(i18n("update"));
updatePane.setValue(UpdateChannel.getChannel());
updatePane.setHasSubtitle(true);
updatePane.setConverter(channel -> i18n("update.channel." + channel.channelName));
updatePane.setItems(List.of(UpdateChannel.STABLE, UpdateChannel.DEVELOPMENT));
updatePane.setDescriptionConverter(channel -> i18n("update.note." + channel.channelName));
final Label lblUpdate;
final Label lblUpdateSub;
{
VBox headerLeft = new VBox();
final StringProperty lblUpdateSubProperty = updatePane.subtitleProperty();
lblUpdate = new Label(i18n("update"));
lblUpdate.getStyleClass().add("title-label");
lblUpdateSub = new Label();
lblUpdateSub.getStyleClass().add("subtitle-label");
headerLeft.getChildren().setAll(lblUpdate, lblUpdateSub);
updatePane.setHeaderLeft(headerLeft);
}
{
updateListener = any -> {
boolean outdated = UpdateChecker.isOutdated();
JFXButton btnUpdate = FXUtils.newToggleButton4(SVG.UPDATE, 20);
btnUpdate.setOnAction(e -> onUpdate());
FXUtils.installFastTooltip(btnUpdate, i18n("update.tooltip"));
updateButton.setVisible(outdated);
updateButton.setManaged(outdated);
updatePane.pseudoClassStateChanged(PseudoClass.getPseudoClass("active"), outdated);
updateListener = any -> {
btnUpdate.setVisible(UpdateChecker.isOutdated());
if (UpdateChecker.isOutdated()) {
lblUpdateSubProperty.set(i18n("update.newest_version", UpdateChecker.getLatestVersion().getVersion()));
lblUpdateSub.setText(i18n("update.newest_version", UpdateChecker.getLatestVersion().getVersion()));
lblUpdateSub.getStyleClass().setAll("update-label");
lblUpdate.setText(i18n("update.found"));
lblUpdate.getStyleClass().setAll("update-label");
} else if (UpdateChecker.isCheckingUpdate()) {
lblUpdateSubProperty.set(i18n("update.checking"));
lblUpdateSub.setText(i18n("update.checking"));
lblUpdateSub.getStyleClass().setAll("subtitle-label");
lblUpdate.setText(i18n("update"));
lblUpdate.getStyleClass().setAll("title-label");
} else {
lblUpdateSubProperty.set(i18n("update.latest"));
lblUpdateSub.setText(i18n("update.latest"));
lblUpdateSub.getStyleClass().setAll("subtitle-label");
lblUpdate.setText(i18n("update"));
lblUpdate.getStyleClass().setAll("title-label");
}
};
UpdateChecker.latestVersionProperty().addListener(new WeakInvalidationListener(updateListener));
UpdateChecker.outdatedProperty().addListener(new WeakInvalidationListener(updateListener));
UpdateChecker.checkingUpdateProperty().addListener(new WeakInvalidationListener(updateListener));
updateListener.invalidated(null);
updatePane.setHeaderRight(btnUpdate);
}
{
VBox content = new VBox(12);
content.setPadding(new Insets(8, 0, 0, 0));
updateChannelGroup = new ToggleGroup();
JFXRadioButton chkUpdateStable = new JFXRadioButton(i18n("update.channel.stable"));
chkUpdateStable.setUserData(UpdateChannel.STABLE);
chkUpdateStable.setToggleGroup(updateChannelGroup);
JFXRadioButton chkUpdateDev = new JFXRadioButton(i18n("update.channel.dev"));
chkUpdateDev.setUserData(UpdateChannel.DEVELOPMENT);
chkUpdateDev.setToggleGroup(updateChannelGroup);
Label noteWrapper = new Label(i18n("update.note"));
VBox.setMargin(noteWrapper, new Insets(8, 0, 0, 0));
content.getChildren().setAll(chkUpdateStable, chkUpdateDev, noteWrapper);
updatePane.getContent().add(content);
}
settingsPane.getContent().add(updatePane);
}
@@ -180,6 +206,8 @@ public final class SettingsPage extends ScrollPane {
previewPane.setSubtitle(i18n("update.preview.subtitle"));
previewPane.selectedProperty().bindBidirectional(config().acceptPreviewUpdateProperty());
ObjectProperty<UpdateChannel> updateChannel = selectedItemPropertyFor(updateChannelGroup, UpdateChannel.class);
updateChannel.set(UpdateChannel.getChannel());
InvalidationListener checkUpdateListener = e -> {
UpdateChecker.requestCheckUpdate(updateChannel.get(), previewPane.isSelected());
};
@@ -282,31 +310,12 @@ public final class SettingsPage extends ScrollPane {
if (LOG.getLogFile() == null)
openLogFolderButton.setDisable(true);
SpinnerPane exportLogPane = new SpinnerPane();
JFXButton logButton = FXUtils.newBorderButton(i18n("settings.launcher.launcher_log.export"));
exportLogPane.setContent(logButton);
logButton.setOnAction(e -> {
exportLogPane.showSpinner();
onExportLogs().whenCompleteAsync((result, exception) -> {
exportLogPane.hideSpinner();
if (exception == null) {
Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", result));
FXUtils.showFileInExplorer(result);
} else {
LOG.warning("Failed to export logs", exception);
Controllers.dialog(
i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception),
null,
MessageType.ERROR
);
}
}, Schedulers.javafx());
});
logButton.setOnAction(e -> onExportLogs());
HBox buttonBox = new HBox();
buttonBox.setSpacing(10);
buttonBox.getChildren().addAll(openLogFolderButton, exportLogPane);
buttonBox.getChildren().addAll(openLogFolderButton, logButton);
BorderPane.setAlignment(buttonBox, Pos.CENTER_RIGHT);
debugPane.setRight(buttonBox);
@@ -378,79 +387,86 @@ public final class SettingsPage extends ScrollPane {
}
}
private CompletableFuture<Path> onExportLogs() {
return CompletableFuture.supplyAsync(Lang.wrap(() -> {
private void onExportLogs() {
thread(() -> {
String nameBase = "hmcl-exported-logs-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss"));
List<Path> recentLogFiles = LOG.findRecentLogFiles(5);
Path outputFile;
if (recentLogFiles.isEmpty()) {
outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".log");
try {
if (recentLogFiles.isEmpty()) {
outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".log");
LOG.info("Exporting latest logs to " + outputFile);
try (OutputStream output = Files.newOutputStream(outputFile)) {
LOG.exportLogs(output);
}
} else {
outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".zip");
LOG.info("Exporting latest logs to " + outputFile);
try (OutputStream output = Files.newOutputStream(outputFile)) {
LOG.exportLogs(output);
}
} else {
outputFile = Metadata.CURRENT_DIRECTORY.resolve(nameBase + ".zip");
LOG.info("Exporting latest logs to " + outputFile);
LOG.info("Exporting latest logs to " + outputFile);
byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
try (var os = Files.newOutputStream(outputFile);
var zos = new ZipOutputStream(os)) {
byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
try (var os = Files.newOutputStream(outputFile);
var zos = new ZipOutputStream(os)) {
Set<String> entryNames = new HashSet<>();
Set<String> entryNames = new HashSet<>();
for (Path path : recentLogFiles) {
String fileName = FileUtils.getName(path);
String extension = StringUtils.substringAfterLast(fileName, '.');
for (Path path : recentLogFiles) {
String fileName = FileUtils.getName(path);
String extension = StringUtils.substringAfterLast(fileName, '.');
if ("gz".equals(extension) || "xz".equals(extension)) {
// If an exception occurs while decompressing the input file, we should
// ensure the input file and the current zip entry are closed,
// then copy the compressed file content as-is into a new entry in the zip file.
if ("gz".equals(extension) || "xz".equals(extension)) {
// If an exception occurs while decompressing the input file, we should
// ensure the input file and the current zip entry are closed,
// then copy the compressed file content as-is into a new entry in the zip file.
InputStream input = null;
try {
input = Files.newInputStream(path);
input = "gz".equals(extension)
? new GZIPInputStream(input)
: new XZInputStream(input);
} catch (Throwable ex) {
LOG.warning("Failed to open log file " + path, ex);
IOUtils.closeQuietly(input, ex);
input = null;
InputStream input = null;
try {
input = Files.newInputStream(path);
input = "gz".equals(extension)
? new GZIPInputStream(input)
: new XZInputStream(input);
} catch (Throwable ex) {
LOG.warning("Failed to open log file " + path, ex);
IOUtils.closeQuietly(input, ex);
input = null;
}
String entryName = getEntryName(entryNames, StringUtils.substringBeforeLast(fileName, "."));
if (input != null && exportLogFile(zos, path, entryName, input, buffer))
continue;
}
String entryName = getEntryName(entryNames, StringUtils.substringBeforeLast(fileName, "."));
if (input != null && exportLogFile(zos, path, entryName, input, buffer))
// Copy the log file content as-is into a new entry in the zip file.
// If an exception occurs while decompressing the input file, we should
// ensure the input file and the current zip entry are closed.
InputStream input;
try {
input = Files.newInputStream(path);
} catch (Throwable ex) {
LOG.warning("Failed to open log file " + path, ex);
continue;
}
exportLogFile(zos, path, getEntryName(entryNames, fileName), input, buffer);
}
// Copy the log file content as-is into a new entry in the zip file.
// If an exception occurs while decompressing the input file, we should
// ensure the input file and the current zip entry are closed.
InputStream input;
try {
input = Files.newInputStream(path);
} catch (Throwable ex) {
LOG.warning("Failed to open log file " + path, ex);
continue;
}
exportLogFile(zos, path, getEntryName(entryNames, fileName), input, buffer);
zos.putNextEntry(new ZipEntry(getEntryName(entryNames, "hmcl-latest.log")));
LOG.exportLogs(zos);
zos.closeEntry();
}
zos.putNextEntry(new ZipEntry(getEntryName(entryNames, "hmcl-latest.log")));
LOG.exportLogs(zos);
zos.closeEntry();
}
} catch (IOException e) {
LOG.warning("Failed to export logs", e);
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(e), null, MessageType.ERROR));
return;
}
return outputFile;
}), Schedulers.io());
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", outputFile)));
FXUtils.showFileInExplorer(outputFile);
});
}
private void onSponsor() {
@@ -22,14 +22,8 @@ import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Skin;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.control.skin.TreeViewSkin;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.glavo.nbt.NBTElement;
import org.glavo.nbt.tag.Tag;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
@@ -39,7 +33,6 @@ import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.IOException;
import java.nio.file.Path;
@@ -63,9 +56,8 @@ public final class NBTEditorPage extends SpinnerPane implements DecoratorPage {
this.state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("nbt.title", file.toString())));
this.file = file;
//noinspection DataFlowIssue
this.type = NBTFileType.ofFile(file);
if (type == null) {
throw new IOException("Unknown type of file " + file);
}
@@ -93,33 +85,14 @@ public final class NBTEditorPage extends SpinnerPane implements DecoratorPage {
actions.getChildren().setAll(saveButton, cancelButton);
Task.supplyAsync(() -> type.read(file))
Task.supplyAsync(() -> type.readAsTree(file))
.whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
setLoading(false);
NBTTreeItem root = new NBTTreeItem(result, FileUtils.getName(file));
var view = new TreeView<>(root) {
@Override
protected Skin<?> createDefaultSkin() {
return new TreeViewSkin<>(this) {
{
FXUtils.smoothScrolling(getVirtualFlow());
}
};
}
};
view.setCellFactory(ignored -> new NBTTreeCell());
view.addEventHandler(TreeItem.<NBTElement>branchExpandedEvent(), event -> {
TreeItem<NBTElement> item = event.getTreeItem();
if (item.getValue() instanceof Tag && item.getChildren().size() == 1)
item.getChildren().get(0).setExpanded(true);
});
root.setExpanded(true);
NBTTreeView view = new NBTTreeView(result);
BorderPane.setMargin(view, new Insets(10));
onEscPressed(view, cancelButton::fire);
this.root.setCenter(view);
root.setCenter(view);
} else {
LOG.warning("Fail to open nbt file", exception);
Controllers.dialog(i18n("nbt.open.failed") + "\n\n" + StringUtils.getStackTrace(exception), null, MessageDialogPane.MessageType.WARNING, cancelButton::fire);
@@ -17,34 +17,148 @@
*/
package org.jackhuang.hmcl.ui.nbt;
import org.glavo.nbt.NBTElement;
import org.glavo.nbt.chunk.ChunkRegion;
import org.glavo.nbt.io.NBTCodec;
import org.glavo.nbt.tag.Tag;
import com.github.steveice10.opennbt.NBTIO;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.IntTag;
import com.github.steveice10.opennbt.tag.builtin.ListTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import kala.compress.utils.BoundedInputStream;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.zip.GZIPInputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
/// @author Glavo
/**
* @author Glavo
*/
public enum NBTFileType {
COMPRESSED("dat", "dat_old") {
@Override
public Tag read(Path file) throws IOException {
return NBTCodec.of().readTag(file);
try (BufferedInputStream fileInputStream = new BufferedInputStream(Files.newInputStream(file))) {
fileInputStream.mark(3);
byte[] header = new byte[3];
if (fileInputStream.read(header) < 3) {
throw new IOException("File is too small");
}
fileInputStream.reset();
InputStream input;
if (Arrays.equals(header, new byte[]{0x1f, (byte) 0x8b, 0x08})) {
input = new GZIPInputStream(fileInputStream);
} else {
input = fileInputStream;
}
Tag tag = NBTIO.readTag(input);
if (!(tag instanceof CompoundTag))
throw new IOException("Unexpected tag: " + tag);
return tag;
}
}
},
ANVIL("mca") {
@Override
public NBTElement read(Path file) throws IOException {
return NBTCodec.of().readRegion(file);
public Tag read(Path file) throws IOException {
return REGION.read(file);
}
@Override
public NBTTreeView.Item readAsTree(Path file) throws IOException {
return REGION.readAsTree(file);
}
},
REGION("mcr") {
@Override
public ChunkRegion read(Path file) throws IOException {
return NBTCodec.of().readRegion(file);
public Tag read(Path file) throws IOException {
try (RandomAccessFile r = new RandomAccessFile(file.toFile(), "r")) {
ListTag tag = new ListTag(file.getFileName().toString(), CompoundTag.class);
if (r.length() == 0) {
return tag;
}
byte[] header = new byte[4096];
byte[] buffer = new byte[1 * 1024 * 1024]; // The maximum size of each chunk is 1MiB
Inflater inflater = new Inflater();
r.readFully(header);
for (int i = 0; i < 4096; i += 4) {
int offset = ((header[i] & 0xff) << 16) + ((header[i + 1] & 0xff) << 8) + (header[i + 2] & 0xff);
int length = header[i + 3] & 0xff;
if (offset == 0 || length == 0) {
continue;
}
r.seek(offset * 4096L);
r.readFully(buffer, 0, length * 4096);
int chunkLength = ((buffer[0] & 0xff) << 24) + ((buffer[1] & 0xff) << 16) + ((buffer[2] & 0xff) << 8) + (buffer[3] & 0xff);
InputStream input = new ByteArrayInputStream(buffer);
input.skip(5);
input = new BoundedInputStream(input, chunkLength - 1);
switch (buffer[4]) {
case 0x01:
// GZip
input = new GZIPInputStream(input);
break;
case 0x02:
// Zlib
inflater.reset();
input = new InflaterInputStream(input, inflater);
break;
case 0x03:
// Uncompressed
break;
default:
throw new IOException("Unsupported compression method: " + Integer.toHexString(buffer[4] & 0xff));
}
try (InputStream in = input) {
Tag chunk = NBTIO.readTag(in);
if (!(chunk instanceof CompoundTag))
throw new IOException("Unexpected tag: " + chunk);
tag.add(chunk);
}
}
return tag;
}
}
@Override
public NBTTreeView.Item readAsTree(Path file) throws IOException {
NBTTreeView.Item item = new NBTTreeView.Item(read(file));
for (Tag tag : ((ListTag) item.getValue())) {
CompoundTag chunk = (CompoundTag) tag;
NBTTreeView.Item tree = NBTTreeView.buildTree(chunk);
Tag xPos = chunk.get("xPos");
Tag zPos = chunk.get("zPos");
if (xPos instanceof IntTag && zPos instanceof IntTag) {
tree.setText(String.format("Chunk: %d %d", xPos.getValue(), zPos.getValue()));
} else {
tree.setText("Chunk: Unknown");
}
item.getChildren().add(tree);
}
return item;
}
};
@@ -54,7 +168,7 @@ public enum NBTFileType {
return NBTFileType.ofFile(file) != null;
}
public static @Nullable NBTFileType ofFile(Path file) {
public static NBTFileType ofFile(Path file) {
String ext = FileUtils.getExtension(file);
for (NBTFileType type : types) {
for (String extension : type.extensions) {
@@ -72,6 +186,11 @@ public enum NBTFileType {
this.extensions = extensions;
}
public abstract NBTElement read(Path file) throws IOException;
public abstract Tag read(Path file) throws IOException;
public NBTTreeView.Item readAsTree(Path file) throws IOException {
NBTTreeView.Item root = NBTTreeView.buildTree(read(file));
root.setName(file.getFileName().toString());
return root;
}
}
@@ -0,0 +1,79 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.nbt;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
/**
* @author Glavo
*/
public enum NBTTagType {
BYTE, SHORT, INT, LONG, FLOAT, DOUBLE,
BYTE_ARRAY, INT_ARRAY, LONG_ARRAY,
STRING,
LIST, COMPOUND;
private static final Map<String, NBTTagType> lookupTable = new HashMap<>();
static {
for (NBTTagType type : values()) {
lookupTable.put(type.getTagClassName(), type);
}
}
public static NBTTagType typeOf(Tag tag) {
NBTTagType type = lookupTable.get(tag.getClass().getSimpleName());
if (type == null) {
throw new IllegalArgumentException("Unknown tag: " + type);
}
return type;
}
private final String iconUrl;
private final String tagClassName;
NBTTagType() {
String tagName;
String className;
int idx = name().indexOf('_');
if (idx < 0) {
tagName = name().charAt(0) + name().substring(1).toLowerCase(Locale.ROOT);
className = tagName + "Tag";
} else {
tagName = name().charAt(0) + name().substring(1, idx + 1).toLowerCase(Locale.ROOT)
+ name().charAt(idx + 1) + name().substring(idx + 2).toLowerCase(Locale.ROOT);
className = tagName.substring(0, idx) + tagName.substring(idx + 1) + "Tag";
}
this.iconUrl = "/assets/img/nbt/TAG_" + tagName + ".png";
this.tagClassName = className;
}
public String getIconUrl() {
return iconUrl;
}
public String getTagClassName() {
return tagClassName;
}
}
@@ -1,129 +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.nbt;
import javafx.scene.control.TreeCell;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import org.glavo.nbt.NBTElement;
import org.glavo.nbt.chunk.Chunk;
import org.glavo.nbt.chunk.ChunkRegion;
import org.glavo.nbt.tag.*;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jetbrains.annotations.Nullable;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class NBTTreeCell extends TreeCell<@Nullable NBTElement> {
private static @Nullable Image getIcon(NBTElement element) {
if (element instanceof Tag tag) {
TagType<?> type = tag.getType();
if (type == TagType.BYTE) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Byte.png");
} else if (type == TagType.SHORT) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Short.png");
} else if (type == TagType.INT) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Int.png");
} else if (type == TagType.LONG) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Long.png");
} else if (type == TagType.FLOAT) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Float.png");
} else if (type == TagType.DOUBLE) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Double.png");
} else if (type == TagType.STRING) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_String.png");
} else if (type == TagType.BYTE_ARRAY) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Byte_Array.png");
} else if (type == TagType.INT_ARRAY) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Int_Array.png");
} else if (type == TagType.LONG_ARRAY) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Long_Array.png");
} else if (type == TagType.LIST) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_List.png");
} else if (type == TagType.COMPOUND) {
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Compound.png");
} else {
return null;
}
} else if (element instanceof ChunkRegion)
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_List.png");
else if (element instanceof Chunk)
return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Compound.png");
else
return null;
}
private final ImageView imageView;
public NBTTreeCell() {
imageView = new ImageView();
imageView.setFitHeight(16);
imageView.setFitWidth(16);
setGraphic(imageView);
}
private NBTTreeItem getNBTTreeItem() {
return (NBTTreeItem) getTreeItem();
}
@Override
public void updateItem(@Nullable NBTElement item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
imageView.setImage(null);
setText(null);
return;
}
imageView.setImage(getIcon(item));
String name = getNBTTreeItem().getOverrideName();
if (name == null) {
NBTElement value = getNBTTreeItem().getValue();
if (value instanceof Tag tag) {
if (tag.getParent() instanceof ListTag<?>) {
name = Integer.toString(tag.getIndex());
} else {
name = tag.getName();
}
} else if (value instanceof Chunk chunk) {
name = "Chunk (" + chunk.getLocalX() + ", " + chunk.getLocalZ() + ")";
} else {
name = "";
}
}
String text;
if (item instanceof ParentTag<?> parentTag) {
text = i18n("nbt.entries", parentTag.size());
} else if (item instanceof ValueTag<?> valueTag) {
text = valueTag.getAsString();
} else {
text = null;
}
if (text == null) {
setText(name);
} else {
setText(name + ": " + text);
}
}
}
@@ -1,68 +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.nbt;
import javafx.collections.ObservableList;
import javafx.scene.control.TreeItem;
import org.glavo.nbt.NBTElement;
import org.glavo.nbt.NBTParent;
import org.glavo.nbt.chunk.Chunk;
import org.glavo.nbt.tag.*;
import org.jetbrains.annotations.Nullable;
/// @author Glavo
public final class NBTTreeItem extends TreeItem<NBTElement> {
private final @Nullable String overrideName;
public NBTTreeItem(NBTElement value) {
this(value, null);
}
public NBTTreeItem(NBTElement value, @Nullable String overrideName) {
super(value);
this.overrideName = overrideName;
}
@Override
public boolean isLeaf() {
return !(getValue() instanceof NBTParent<?> parent) || parent.isEmpty();
}
private boolean needInitChildren = true;
@Override
public ObservableList<TreeItem<NBTElement>> getChildren() {
ObservableList<TreeItem<NBTElement>> children = super.getChildren();
if (needInitChildren) {
needInitChildren = false;
if (getValue() instanceof Chunk chunk) {
if (chunk.getRootTag() != null) {
children.setAll(chunk.getRootTag().stream().map(NBTTreeItem::new).toList());
}
} else if (getValue() instanceof NBTParent<?> parent) {
children.setAll(parent.stream().map(NBTTreeItem::new).toList());
}
}
return children;
}
public @Nullable String getOverrideName() {
return overrideName;
}
}
@@ -0,0 +1,184 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.nbt;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.ListTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.control.skin.TreeViewSkin;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.util.Callback;
import org.jackhuang.hmcl.ui.FXUtils;
import java.lang.reflect.Array;
import java.util.EnumMap;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
* @author Glavo
*/
public final class NBTTreeView extends TreeView<Tag> {
public NBTTreeView(NBTTreeView.Item tree) {
this.setRoot(tree);
if (tree != null) tree.setExpanded(true);
this.setCellFactory(cellFactory());
}
@Override
protected javafx.scene.control.Skin<?> createDefaultSkin() {
return new TreeViewSkin<Tag>(this) {
{
FXUtils.smoothScrolling(getVirtualFlow());
}
};
}
private static Callback<TreeView<Tag>, TreeCell<Tag>> cellFactory() {
EnumMap<NBTTagType, Image> icons = new EnumMap<>(NBTTagType.class);
return view -> new TreeCell<>() {
private void setTagText(String text) {
String name = ((Item) getTreeItem()).getName();
if (name == null) {
setText(text);
} else if (text == null) {
setText(name);
} else {
setText(name + ": " + text);
}
}
private void setTagText(int nEntries) {
setTagText(i18n("nbt.entries", nEntries));
}
@Override
public void updateItem(Tag item, boolean empty) {
super.updateItem(item, empty);
ImageView imageView = (ImageView) this.getGraphic();
if (imageView == null) {
imageView = new ImageView();
this.setGraphic(imageView);
}
if (item == null) {
imageView.setImage(null);
setText(null);
return;
}
NBTTagType tagType = NBTTagType.typeOf(item);
imageView.setImage(icons.computeIfAbsent(tagType, type -> new Image(type.getIconUrl())));
imageView.setFitHeight(16);
imageView.setFitWidth(16);
if (((Item) getTreeItem()).getText() != null) {
setText(((Item) getTreeItem()).getText());
} else {
switch (tagType) {
case BYTE:
case SHORT:
case INT:
case LONG:
case FLOAT:
case DOUBLE:
case STRING:
setTagText(item.getValue().toString());
break;
case BYTE_ARRAY:
case INT_ARRAY:
case LONG_ARRAY:
setTagText(Array.getLength(item.getValue()));
break;
case LIST:
setTagText(((ListTag) item).size());
break;
case COMPOUND:
setTagText(((CompoundTag) item).size());
break;
default:
setTagText(null);
}
}
}
};
}
public static Item buildTree(Tag tag) {
Item item = new Item(tag);
if (tag instanceof CompoundTag) {
for (Tag subTag : ((CompoundTag) tag)) {
item.getChildren().add(buildTree(subTag));
}
} else if (tag instanceof ListTag) {
int idx = 0;
for (Tag subTag : ((ListTag) tag)) {
Item subTree = buildTree(subTag);
subTree.setName(String.valueOf(idx++));
item.getChildren().add(subTree);
}
}
FXUtils.onChangeAndOperate(item.expandedProperty(), expanded -> {
if (expanded && item.getChildren().size() == 1) item.getChildren().get(0).setExpanded(true);
});
return item;
}
public CompoundTag getRootTag() {
return ((CompoundTag) getRoot().getValue());
}
public static class Item extends TreeItem<Tag> {
private String text;
private String name;
public Item() {
}
public Item(Tag value) {
super(value);
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name == null ? getValue().getName() : name;
}
}
}
@@ -1,22 +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/>.
*/
@NotNullByDefault
package org.jackhuang.hmcl.ui.nbt;
import org.jetbrains.annotations.NotNullByDefault;
@@ -1,20 +1,3 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXTextField;
@@ -212,11 +195,9 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
useNativeGLFWPane = new LineToggleButton();
useNativeGLFWPane.setTitle(i18n("settings.advanced.use_native_glfw"));
useNativeGLFWPane.setSubtitle(i18n("settings.advanced.linux_freebsd_only"));
useNativeOpenALPane = new LineToggleButton();
useNativeOpenALPane.setTitle(i18n("settings.advanced.use_native_openal"));
useNativeOpenALPane.setSubtitle(i18n("settings.advanced.linux_freebsd_only"));
workaroundPane.getContent().setAll(
nativesDirSublist, rendererPane, noJVMArgsPane, noOptimizingJVMArgsPane, noGameCheckPane,
@@ -22,7 +22,7 @@ import javafx.collections.ObservableList;
import javafx.scene.control.Skin;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.mod.DataPack;
import org.jackhuang.hmcl.mod.Datapack;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
@@ -44,45 +44,45 @@ import java.util.regex.Pattern;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class DataPackListPage extends ListPageBase<DataPackListPageSkin.DataPackInfoObject> implements WorldManagePage.WorldRefreshable {
public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.DatapackInfoObject> implements WorldManagePage.WorldRefreshable {
private final World world;
private final DataPack dataPack;
private final Datapack datapack;
final BooleanProperty readOnly;
public DataPackListPage(WorldManagePage worldManagePage) {
public DatapackListPage(WorldManagePage worldManagePage) {
world = worldManagePage.getWorld();
dataPack = new DataPack(world.getFile().resolve("datapacks"));
setItems(MappedObservableList.create(dataPack.getPacks(), DataPackListPageSkin.DataPackInfoObject::new));
datapack = new Datapack(world.getFile().resolve("datapacks"));
setItems(MappedObservableList.create(datapack.getPacks(), DatapackListPageSkin.DatapackInfoObject::new));
readOnly = worldManagePage.readOnlyProperty();
FXUtils.applyDragListener(this, it -> Objects.equals("zip", FileUtils.getExtension(it)),
this::installMultiDataPack, this::refresh);
this::installMultiDatapack, this::refresh);
refresh();
}
private void installMultiDataPack(List<Path> dataPackPath) {
dataPackPath.forEach(this::installSingleDataPack);
private void installMultiDatapack(List<Path> datapackPath) {
datapackPath.forEach(this::installSingleDatapack);
if (readOnly.get()) {
Controllers.showToast(i18n("datapack.reload.toast"));
}
}
private void installSingleDataPack(Path dataPack) {
private void installSingleDatapack(Path datapack) {
try {
this.dataPack.installPack(dataPack, world.getGameVersion());
this.datapack.installPack(datapack, world.getGameVersion());
} catch (IOException | IllegalArgumentException e) {
LOG.warning("Unable to parse datapack file " + dataPack, e);
LOG.warning("Unable to parse datapack file " + datapack, e);
}
}
@Override
protected Skin<?> createDefaultSkin() {
return new DataPackListPageSkin(this);
return new DatapackListPageSkin(this);
}
public void refresh() {
setLoading(true);
Task.runAsync(dataPack::loadFromDir)
Task.runAsync(datapack::loadFromDir)
.withRunAsync(Schedulers.javafx(), () -> setLoading(false))
.start();
}
@@ -94,18 +94,18 @@ public final class DataPackListPage extends ListPageBase<DataPackListPageSkin.Da
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
if (res != null) {
installMultiDataPack(res);
installMultiDatapack(res);
}
dataPack.loadFromDir();
datapack.loadFromDir();
}
void removeSelected(ObservableList<DataPackListPageSkin.DataPackInfoObject> selectedItems) {
void removeSelected(ObservableList<DatapackListPageSkin.DatapackInfoObject> selectedItems) {
selectedItems.stream()
.map(DataPackListPageSkin.DataPackInfoObject::getPackInfo)
.map(DatapackListPageSkin.DatapackInfoObject::getPackInfo)
.forEach(pack -> {
try {
dataPack.deletePack(pack);
datapack.deletePack(pack);
} catch (IOException e) {
// Fail to remove mods if the game is running or the datapack is absent.
LOG.warning("Failed to delete datapack \"" + pack.getId() + "\"", e);
@@ -113,23 +113,23 @@ public final class DataPackListPage extends ListPageBase<DataPackListPageSkin.Da
});
}
void enableSelected(ObservableList<DataPackListPageSkin.DataPackInfoObject> selectedItems) {
void enableSelected(ObservableList<DatapackListPageSkin.DatapackInfoObject> selectedItems) {
selectedItems.stream()
.map(DataPackListPageSkin.DataPackInfoObject::getPackInfo)
.map(DatapackListPageSkin.DatapackInfoObject::getPackInfo)
.forEach(pack -> pack.setActive(true));
}
void disableSelected(ObservableList<DataPackListPageSkin.DataPackInfoObject> selectedItems) {
void disableSelected(ObservableList<DatapackListPageSkin.DatapackInfoObject> selectedItems) {
selectedItems.stream()
.map(DataPackListPageSkin.DataPackInfoObject::getPackInfo)
.map(DatapackListPageSkin.DatapackInfoObject::getPackInfo)
.forEach(pack -> pack.setActive(false));
}
void openDataPackFolder() {
FXUtils.openFolder(dataPack.getPath());
FXUtils.openFolder(datapack.getPath());
}
@NotNull Predicate<DataPackListPageSkin.DataPackInfoObject> updateSearchPredicate(String queryString) {
@NotNull Predicate<DatapackListPageSkin.DatapackInfoObject> updateSearchPredicate(String queryString) {
if (queryString.isBlank()) {
return dataPack -> true;
}
@@ -42,7 +42,7 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import org.jackhuang.hmcl.mod.DataPack;
import org.jackhuang.hmcl.mod.Datapack;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -67,7 +67,7 @@ import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
private final TransitionPane toolbarPane;
private final HBox searchBar;
@@ -75,8 +75,8 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
private final HBox selectingToolbar;
InvalidationListener updateBarByStateWeakListener;
private final JFXListView<DataPackInfoObject> listView;
private final FilteredList<DataPackInfoObject> filteredList;
private final JFXListView<DatapackInfoObject> listView;
private final FilteredList<DatapackInfoObject> filteredList;
private final BooleanProperty isSearching = new SimpleBooleanProperty(false);
private final BooleanProperty isSelecting = new SimpleBooleanProperty(false);
@@ -85,7 +85,7 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
private static final AtomicInteger lastShiftClickIndex = new AtomicInteger(-1);
final Consumer<Integer> toggleSelect;
DataPackListPageSkin(DataPackListPage skinnable) {
DatapackListPageSkin(DatapackListPage skinnable) {
super(skinnable);
StackPane pane = new StackPane();
@@ -181,7 +181,7 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
ComponentList.setVgrow(center, Priority.ALWAYS);
center.loadingProperty().bind(skinnable.loadingProperty());
listView.setCellFactory(x -> new DataPackInfoListCell(listView, getSkinnable().readOnly));
listView.setCellFactory(x -> new DatapackInfoListCell(listView, getSkinnable().readOnly));
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
this.listView.setItems(filteredList);
@@ -216,13 +216,13 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
}
}
static class DataPackInfoObject extends RecursiveTreeObject<DataPackInfoObject> {
static class DatapackInfoObject extends RecursiveTreeObject<DatapackInfoObject> {
private final BooleanProperty activeProperty;
private final DataPack.Pack packInfo;
private final Datapack.Pack packInfo;
private SoftReference<CompletableFuture<Image>> iconCache;
DataPackInfoObject(DataPack.Pack packInfo) {
DatapackInfoObject(Datapack.Pack packInfo) {
this.packInfo = packInfo;
this.activeProperty = packInfo.activeProperty();
}
@@ -235,7 +235,7 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
return packInfo.getDescription().toString();
}
DataPack.Pack getPackInfo() {
Datapack.Pack getPackInfo() {
return packInfo;
}
@@ -270,7 +270,7 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
}
}
public void loadIcon(ImageContainer imageContainer, @Nullable WeakReference<ObjectProperty<DataPackInfoObject>> current) {
public void loadIcon(ImageContainer imageContainer, @Nullable WeakReference<ObjectProperty<DatapackInfoObject>> current) {
SoftReference<CompletableFuture<Image>> iconCache = this.iconCache;
CompletableFuture<Image> imageFuture;
if (iconCache != null && (imageFuture = iconCache.get()) != null) {
@@ -286,7 +286,7 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
imageContainer.setImage(FXUtils.newBuiltinImage("/assets/img/unknown_pack.png"));
imageFuture.thenAcceptAsync(image -> {
if (current != null) {
ObjectProperty<DataPackInfoObject> infoObjectProperty = current.get();
ObjectProperty<DatapackInfoObject> infoObjectProperty = current.get();
if (infoObjectProperty == null || infoObjectProperty.get() != this) {
// The current ListCell has already switched to another object
return;
@@ -298,13 +298,13 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
}
}
private final class DataPackInfoListCell extends MDListCell<DataPackInfoObject> {
private final class DatapackInfoListCell extends MDListCell<DatapackInfoObject> {
final JFXCheckBox checkBox = new JFXCheckBox();
ImageContainer imageContainer = new ImageContainer(32);
final TwoLineListItem content = new TwoLineListItem();
BooleanProperty booleanProperty;
DataPackInfoListCell(JFXListView<DataPackInfoObject> listView, BooleanProperty isReadOnlyProperty) {
DatapackInfoListCell(JFXListView<DatapackInfoObject> listView, BooleanProperty isReadOnlyProperty) {
super(listView);
HBox container = new HBox(8);
@@ -326,7 +326,7 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
}
@Override
protected void updateControl(DataPackInfoObject dataItem, boolean empty) {
protected void updateControl(DatapackInfoObject dataItem, boolean empty) {
if (empty) return;
content.setTitle(dataItem.getTitle());
content.setSubtitle(dataItem.getSubtitle());
@@ -338,7 +338,7 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
}
}
public void handleSelect(DataPackInfoListCell cell, MouseEvent mouseEvent) {
public void handleSelect(DatapackInfoListCell cell, MouseEvent mouseEvent) {
if (cell.isEmpty()) {
mouseEvent.consume();
return;
@@ -27,8 +27,16 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
@@ -46,7 +54,11 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.SimpleMultimap;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
@@ -54,7 +66,14 @@ import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -294,55 +313,25 @@ public class DownloadPage extends Control implements DecoratorPage {
}
}
final class Versions {
static ComponentSublist createSublist(DownloadPage control, String title, List<String> target) {
var sublist = new ComponentSublist(() -> {
ArrayList<ModItem> items = new ArrayList<>();
for (String gv : target) {
List<RemoteMod.Version> versions = control.versions.get(gv);
if (versions != null) {
for (RemoteMod.Version v : versions) {
items.add(new ModItem(control.addon, v, control));
}
}
}
return items;
});
sublist.setTitle(title);
sublist.getStyleClass().add("no-padding");
return sublist;
for (String gameVersion : control.versions.keys().stream()
.sorted(Collections.reverseOrder(GameVersionNumber::compare))
.toList()) {
List<RemoteMod.Version> versions = control.versions.get(gameVersion);
if (versions == null || versions.isEmpty()) {
continue;
}
final List<String> releases = new ArrayList<>();
final List<String> snapshots = new ArrayList<>();
}
var sublist = new ComponentSublist(() -> {
ArrayList<ModItem> items = new ArrayList<>(versions.size());
for (RemoteMod.Version v : versions) {
items.add(new ModItem(control.addon, v, control));
}
return items;
});
sublist.getStyleClass().add("no-padding");
sublist.setTitle("Minecraft " + gameVersion);
TreeMap<GameVersionNumber, Versions> map = new TreeMap<>(Collections.reverseOrder());
for (String version : control.versions.keys()) {
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(version);
GameVersionNumber releaseOfSnapshot = GameVersionNumber.getReleaseOfSnapshot(gameVersion);
GameVersionNumber releaseVersion = Objects.requireNonNullElse(releaseOfSnapshot, gameVersion);
Versions lists = map.computeIfAbsent(releaseVersion, v -> new Versions());
if (releaseOfSnapshot == null) {
lists.releases.add(version);
} else {
lists.snapshots.add(version);
}
}
for (Map.Entry<GameVersionNumber, Versions> entry : map.entrySet()) {
GameVersionNumber gameVersion = entry.getKey();
Versions versions = entry.getValue();
if (!versions.releases.isEmpty())
list.getContent().add(Versions.createSublist(control, i18n("addon.download.title.release", gameVersion), versions.releases));
if (!versions.snapshots.isEmpty())
list.getContent().add(Versions.createSublist(control, i18n("addon.download.title.snapshot", gameVersion), versions.snapshots));
list.getContent().add(sublist);
}
});
}
@@ -25,7 +25,6 @@ import org.jackhuang.hmcl.util.i18n.I18n;
import java.util.MissingResourceException;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -62,30 +61,13 @@ public final class HMCLLocalizedDownloadListPage extends DownloadListPage {
repository = new Repository(type, curseForge, modrinth);
supportChinese.set(true);
boolean supportedCurseForge = CurseForgeRemoteModRepository.isAvailable() && curseForge != null;
downloadSources.setAll("mods.modrinth");
if (supportedCurseForge) {
downloadSources.add("mods.curseforge");
}
if ("curseforge".equalsIgnoreCase(config().getDefaultAddonSource())) {
if (supportedCurseForge) {
downloadSource.set("mods.curseforge");
} else if (modrinth != null) {
downloadSource.set("mods.modrinth");
} else {
throw new AssertionError("Should not be here.");
}
downloadSources.get().setAll("mods.modrinth", "mods.curseforge");
if (modrinth != null) {
downloadSource.set("mods.modrinth");
} else if (curseForge != null) {
downloadSource.set("mods.curseforge");
} else {
if (modrinth != null) {
downloadSource.set("mods.modrinth");
} else if (supportedCurseForge) {
downloadSource.set("mods.curseforge");
} else {
throw new AssertionError("Should not be here.");
}
throw new AssertionError("Should not be here.");
}
}
@@ -1,20 +1,3 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.JFXButton;
@@ -30,10 +30,8 @@ import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.EventPriority;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
@@ -41,10 +39,8 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.nio.file.Path;
import java.util.Optional;
import java.util.function.Supplier;
@@ -190,34 +186,15 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
}
private void clearLibraries() {
var libraries = getProfile().getRepository().getBaseDirectory().resolve("libraries");
Task.runAsync(Schedulers.io(), () -> {
FileUtils.deleteDirectoryQuietly(libraries);
}).whenComplete(Schedulers.javafx(), (exception) -> {
if (exception != null) {
Controllers.dialog(i18n("message.failed") + "\n" + StringUtils.getStackTrace(exception), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
}
}).start();
FileUtils.deleteDirectoryQuietly(getProfile().getRepository().getBaseDirectory().resolve("libraries"));
}
private void clearAssets() {
Path assetsDir = getProfile().getRepository().getBaseDirectory().resolve("assets");
Profile.ProfileVersion currentVersion = version.get();
Path resourcesDir = currentVersion != null
? getProfile().getRepository().getRunDirectory(currentVersion.getVersion()).resolve("resources")
: null;
Task.runAsync(Schedulers.io(), () -> {
FileUtils.deleteDirectoryQuietly(assetsDir);
if (resourcesDir != null) {
FileUtils.deleteDirectoryQuietly(resourcesDir);
}
}).whenComplete(Schedulers.javafx(), (exception) -> {
if (exception != null) {
Controllers.dialog(i18n("message.failed") + "\n" + StringUtils.getStackTrace(exception), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
}
}).start();
HMCLGameRepository baseDirectory = getProfile().getRepository();
FileUtils.deleteDirectoryQuietly(baseDirectory.getBaseDirectory().resolve("assets"));
if (version.get() != null) {
FileUtils.deleteDirectoryQuietly(baseDirectory.getRunDirectory(version.get().getVersion()).resolve("resources"));
}
}
private void clearJunkFiles() {
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.*;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
@@ -42,7 +43,6 @@ import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.MemoryStatusBar;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
@@ -57,6 +57,7 @@ import org.jackhuang.hmcl.util.platform.SystemInfo;
import org.jackhuang.hmcl.util.platform.hardware.PhysicalMemoryStatus;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.lang.ref.WeakReference;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -114,6 +115,8 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
private final IntegerProperty maxMemory = new SimpleIntegerProperty();
private final BooleanProperty modpack = new SimpleBooleanProperty();
private final ReadOnlyObjectProperty<PhysicalMemoryStatus> memoryStatus = UpdateMemoryStatus.memoryStatusProperty();
public VersionSettingsPage(boolean globalSetting) {
ScrollPane scrollPane = new ScrollPane();
scrollPane.setFitToHeight(true);
@@ -293,14 +296,30 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
lowerBoundPane.getChildren().setAll(label, slider, txtMaxMemory, new Label(i18n("settings.memory.unit.mib")));
}
MemoryStatusBar memoryStatusBar = new MemoryStatusBar();
VBox.setMargin(memoryStatusBar, new Insets(8, 0, 0, 16));
memoryStatusBar.memoryAllocatedProperty().bind(Bindings.createDoubleBinding(() ->
(double) HMCLGameRepository.getAllocatedMemory(
maxMemory.get() * 1024L * 1024L,
memoryStatusBar.getMemoryStatus().getAvailable(),
chkAutoAllocate.isSelected()),
memoryStatusBar.memoryStatusProperty(), maxMemory, chkAutoAllocate.selectedProperty()));
StackPane progressBarPane = new StackPane();
progressBarPane.setAlignment(Pos.CENTER_LEFT);
VBox.setMargin(progressBarPane, new Insets(8, 0, 0, 16));
{
progressBarPane.setMinHeight(4);
progressBarPane.getStyleClass().add("memory-total");
StackPane usedMemory = new StackPane();
usedMemory.getStyleClass().add("memory-used");
usedMemory.maxWidthProperty().bind(Bindings.createDoubleBinding(() ->
progressBarPane.getWidth() *
(memoryStatus.get().getUsed() * 1.0 / memoryStatus.get().getTotal()), progressBarPane.widthProperty(),
memoryStatus));
StackPane allocateMemory = new StackPane();
allocateMemory.getStyleClass().add("memory-allocate");
allocateMemory.maxWidthProperty().bind(Bindings.createDoubleBinding(() ->
progressBarPane.getWidth() *
Math.min(1.0,
(double) (HMCLGameRepository.getAllocatedMemory(maxMemory.get() * 1024L * 1024L, memoryStatus.get().getAvailable(), chkAutoAllocate.isSelected())
+ memoryStatus.get().getUsed()) / memoryStatus.get().getTotal()), progressBarPane.widthProperty(),
maxMemory, memoryStatus, chkAutoAllocate.selectedProperty()));
progressBarPane.getChildren().setAll(allocateMemory, usedMemory);
}
BorderPane digitalPane = new BorderPane();
VBox.setMargin(digitalPane, new Insets(0, 0, 0, 16));
@@ -308,29 +327,26 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
Label lblPhysicalMemory = new Label();
lblPhysicalMemory.getStyleClass().add("memory-label");
digitalPane.setLeft(lblPhysicalMemory);
lblPhysicalMemory.textProperty().bind(Bindings.createStringBinding(() -> {
PhysicalMemoryStatus memoryStatus = memoryStatusBar.getMemoryStatus();
return i18n("settings.memory.used_per_total",
GIGABYTES.convertFromBytes(memoryStatus.getUsed()),
GIGABYTES.convertFromBytes(memoryStatus.getTotal()));
}, memoryStatusBar.memoryStatusProperty()));
lblPhysicalMemory.textProperty().bind(Bindings.createStringBinding(() ->
i18n("settings.memory.used_per_total",
GIGABYTES.convertFromBytes(memoryStatus.get().getUsed()),
GIGABYTES.convertFromBytes(memoryStatus.get().getTotal())), memoryStatus));
Label lblAllocateMemory = new Label();
lblAllocateMemory.textProperty().bind(Bindings.createStringBinding(() -> {
PhysicalMemoryStatus memoryStatus = memoryStatusBar.getMemoryStatus();
long maxMemory = Lang.parseInt(this.maxMemory.get(), 0) * 1024L * 1024L;
return i18n(memoryStatus.hasAvailable() && maxMemory > memoryStatus.getAvailable()
return i18n(memoryStatus.get().hasAvailable() && maxMemory > memoryStatus.get().getAvailable()
? (chkAutoAllocate.isSelected() ? "settings.memory.allocate.auto.exceeded" : "settings.memory.allocate.manual.exceeded")
: (chkAutoAllocate.isSelected() ? "settings.memory.allocate.auto" : "settings.memory.allocate.manual"),
GIGABYTES.convertFromBytes(maxMemory),
GIGABYTES.convertFromBytes(HMCLGameRepository.getAllocatedMemory(maxMemory, memoryStatus.getAvailable(), chkAutoAllocate.isSelected())),
GIGABYTES.convertFromBytes(memoryStatus.getAvailable()));
}, memoryStatusBar.memoryStatusProperty(), maxMemory, chkAutoAllocate.selectedProperty()));
GIGABYTES.convertFromBytes(HMCLGameRepository.getAllocatedMemory(maxMemory, memoryStatus.get().getAvailable(), chkAutoAllocate.isSelected())),
GIGABYTES.convertFromBytes(memoryStatus.get().getAvailable()));
}, memoryStatus, maxMemory, chkAutoAllocate.selectedProperty()));
lblAllocateMemory.getStyleClass().add("memory-label");
digitalPane.setRight(lblAllocateMemory);
}
maxMemoryPane.getChildren().setAll(title, chkAutoAllocate, lowerBoundPane, memoryStatusBar, digitalPane);
maxMemoryPane.getChildren().setAll(title, chkAutoAllocate, lowerBoundPane, progressBarPane, digitalPane);
}
launcherVisibilityPane = new LineSelectButton<>();
@@ -456,7 +472,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
componentList.disableProperty().bind(enableSpecificSettings.not());
}
private void fireWorkingDirChanged() {
FXUtils.runInFX(() -> fireEvent(new VersionPage.WorkingDirChangedEvent()));
}
@@ -760,4 +776,57 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
return state.getReadOnlyProperty();
}
private static final class UpdateMemoryStatus extends Thread {
@FXThread
private static WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyCache;
@FXThread
static ReadOnlyObjectProperty<PhysicalMemoryStatus> memoryStatusProperty() {
if (memoryStatusPropertyCache != null) {
var property = memoryStatusPropertyCache.get();
if (property != null) {
return property;
}
}
ObjectProperty<PhysicalMemoryStatus> property = new SimpleObjectProperty<>(PhysicalMemoryStatus.INVALID);
memoryStatusPropertyCache = new WeakReference<>(property);
new UpdateMemoryStatus(memoryStatusPropertyCache).start();
return property;
}
private final WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyRef;
UpdateMemoryStatus(WeakReference<ObjectProperty<PhysicalMemoryStatus>> memoryStatusPropertyRef) {
this.memoryStatusPropertyRef = memoryStatusPropertyRef;
setName("UpdateMemoryStatus");
setDaemon(true);
setPriority(Thread.MIN_PRIORITY);
}
@Override
public void run() {
while (true) {
PhysicalMemoryStatus status = SystemInfo.getPhysicalMemoryStatus();
var memoryStatusProperty = memoryStatusPropertyRef.get();
if (memoryStatusProperty == null)
return;
if (Controllers.isStopped())
return;
Platform.runLater(() -> memoryStatusProperty.set(status));
try {
//noinspection BusyWait
Thread.sleep(1000);
} catch (InterruptedException e) {
return;
}
}
}
}
}
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.ui.versions;
import com.github.steveice10.opennbt.tag.builtin.*;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.property.SimpleBooleanProperty;
@@ -33,7 +34,6 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import org.glavo.nbt.tag.*;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -76,7 +76,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
}
private void updateControls() {
CompoundTag dataTag = (CompoundTag) levelData.get("Data");
CompoundTag dataTag = levelData.get("Data");
CompoundTag playerTag = playerData;
ScrollPane scrollPane = new ScrollPane();
@@ -100,7 +100,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
setRightTextField(worldNamePane, worldNameField, 200);
if (dataTag.get("LevelName") instanceof StringTag worldNameTag) {
var worldName = new SimpleStringProperty(worldNameTag.get());
var worldName = new SimpleStringProperty(worldNameTag.getValue());
FXUtils.bindString(worldNameField, worldName);
worldNameField.getProperties().put(WorldInfoPage.class.getName() + ".worldNameProperty", worldName);
worldName.addListener((observable, oldValue, newValue) -> {
@@ -324,7 +324,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
{
locationPane.setTitle(i18n("world.info.player.location"));
Dimension dimension = Dimension.of(playerTag.get("Dimension"));
if (dimension != null && playerTag.get("Pos") instanceof ListTag<?> posTag) {
if (dimension != null && playerTag.get("Pos") instanceof ListTag posTag) {
locationPane.setText(dimension.formatPosition(posTag));
}
}
@@ -347,7 +347,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
if (playerTag.get("respawn") instanceof CompoundTag respawnTag
&& respawnTag.get("dimension") instanceof StringTag dimensionTag
&& respawnTag.get("pos") instanceof IntArrayTag intArrayTag
&& intArrayTag.size() >= 3) { // Valid after 25w07a
&& intArrayTag.length() >= 3) { // Valid after 25w07a
spawnPane.setText(Dimension.of(dimensionTag).formatPosition(intArrayTag));
} else if (playerTag.get("SpawnX") instanceof IntTag intX
&& playerTag.get("SpawnY") instanceof IntTag intY
@@ -541,7 +541,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
static Dimension of(Tag tag) {
if (tag instanceof IntTag intTag) {
return switch (intTag.get()) {
return switch (intTag.getValue()) {
case 0 -> OVERWORLD;
case -1 -> THE_NETHER;
case 1 -> THE_END;
@@ -561,23 +561,30 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
}
String formatPosition(Tag tag) {
if (tag instanceof ListTag<?> listTag && listTag.size() == 3) {
if (tag instanceof ListTag listTag && listTag.size() == 3) {
if (listTag.getTag(0) instanceof DoubleTag x
&& listTag.getTag(1) instanceof DoubleTag y
&& listTag.getTag(2) instanceof DoubleTag z) {
return formatPosition(x.get(), y.get(), z.get());
Tag x = listTag.get(0);
Tag y = listTag.get(1);
Tag z = listTag.get(2);
if (x instanceof DoubleTag && y instanceof DoubleTag && z instanceof DoubleTag) {
return this == OVERWORLD
? String.format("(%.2f, %.2f, %.2f)", x.getValue(), y.getValue(), z.getValue())
: String.format("%s (%.2f, %.2f, %.2f)", name, x.getValue(), y.getValue(), z.getValue());
}
return null;
}
if (tag instanceof IntArrayTag intArrayTag) {
int x = intArrayTag.get(0);
int y = intArrayTag.get(1);
int z = intArrayTag.get(2);
return formatPosition(x, y, z);
int x = intArrayTag.getValue(0);
int y = intArrayTag.getValue(1);
int z = intArrayTag.getValue(2);
return this == OVERWORLD
? String.format("(%d, %d, %d)", x, y, z)
: String.format("%s (%d, %d, %d)", name, x, y, z);
}
return null;
@@ -585,14 +592,14 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.
String formatPosition(int x, int y, int z) {
return this == OVERWORLD
? "(%d, %d, %d)".formatted(x, y, z)
: "%s (%d, %d, %d)".formatted(name, x, y, z);
? String.format("(%d, %d, %d)", x, y, z)
: String.format("%s (%d, %d, %d)", name, x, y, z);
}
String formatPosition(double x, double y, double z) {
return this == OVERWORLD
? "(%.2f, %.2f, %.2f)".formatted(x, y, z)
: "%s (%.2f, %.2f, %.2f)".formatted(name, x, y, z);
? String.format("(%.2f, %.2f, %.2f)", x, y, z)
: String.format("%s (%.2f, %.2f, %.2f)", name, x, y, z);
}
}
@@ -67,7 +67,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
private final TabHeader header = new TabHeader(transitionPane);
private final TabHeader.Tab<WorldInfoPage> worldInfoTab = new TabHeader.Tab<>("worldInfoPage");
private final TabHeader.Tab<WorldBackupsPage> worldBackupsTab = new TabHeader.Tab<>("worldBackupsPage");
private final TabHeader.Tab<DataPackListPage> dataPackTab = new TabHeader.Tab<>("dataPackListPage");
private final TabHeader.Tab<DatapackListPage> datapackTab = new TabHeader.Tab<>("datapackListPage");
public WorldManagePage(World world, Profile profile, String instanceId) {
this.world = world;
@@ -86,7 +86,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
worldInfoTab.setNodeSupplier(() -> new WorldInfoPage(this));
worldBackupsTab.setNodeSupplier(() -> new WorldBackupsPage(this));
dataPackTab.setNodeSupplier(() -> new DataPackListPage(this));
datapackTab.setNodeSupplier(() -> new DatapackListPage(this));
this.state = new SimpleObjectProperty<>(new State(i18n("world.manage.title", StringUtils.parseColorEscapes(world.getWorldName())), null, true, true, true));
@@ -218,9 +218,9 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
tabBar.addNavigationDrawerTab(getSkinnable().header, getSkinnable().worldInfoTab, i18n("world.info"), SVG.INFO, SVG.INFO_FILL)
.addNavigationDrawerTab(getSkinnable().header, getSkinnable().worldBackupsTab, i18n("world.backup"), SVG.ARCHIVE, SVG.ARCHIVE_FILL);
if (getSkinnable().world.supportDataPacks()) {
getSkinnable().header.getTabs().add(getSkinnable().dataPackTab);
tabBar.addNavigationDrawerTab(getSkinnable().header, getSkinnable().dataPackTab, i18n("world.datapack"), SVG.EXTENSION, SVG.EXTENSION_FILL);
if (getSkinnable().world.supportDatapacks()) {
getSkinnable().header.getTabs().add(getSkinnable().datapackTab);
tabBar.addNavigationDrawerTab(getSkinnable().header, getSkinnable().datapackTab, i18n("world.datapack"), SVG.EXTENSION, SVG.EXTENSION_FILL);
}
}
@@ -67,7 +67,7 @@ public final class UpdateHandler {
performMigration();
} catch (IOException e) {
LOG.warning("Failed to perform migration", e);
SwingUtils.showErrorDialog(i18n("fatal.apply_update_failure", Metadata.MANUAL_UPDATE_URL) + "\n" + StringUtils.getStackTrace(e));
SwingUtils.showErrorDialog(i18n("fatal.apply_update_failure", Metadata.PUBLISH_URL) + "\n" + StringUtils.getStackTrace(e));
}
return true;
}
@@ -82,7 +82,7 @@ public final class UpdateHandler {
applyUpdate(Paths.get(args[1]));
} catch (IOException e) {
LOG.warning("Failed to apply update", e);
SwingUtils.showErrorDialog(i18n("fatal.apply_update_failure", Metadata.MANUAL_UPDATE_URL) + "\n" + StringUtils.getStackTrace(e));
SwingUtils.showErrorDialog(i18n("fatal.apply_update_failure", Metadata.PUBLISH_URL) + "\n" + StringUtils.getStackTrace(e));
}
return true;
}
@@ -45,9 +45,9 @@
"externalLink" : "https://github.com/jenkinsci/constant-pool-scanner"
},
{
"title" : "HelloNBT",
"subtitle" : "Copyright © 2026 Glavo.\nLicensed under the Apache 2.0 License.",
"externalLink" : "https://github.com/Glavo/HelloNBT"
"title" : "OpenNBT",
"subtitle" : "Copyright © 2013-2021 Steveice10.\nLicensed under the MIT License.",
"externalLink" : "https://github.com/GeyserMC/OpenNBT"
},
{
"title" : "minecraft-jfx-skin",
@@ -69,11 +69,6 @@
"subtitle" : "Copyright © 2015 Andrew Ellerton.\nLicensed under the Apache 2.0 License.",
"externalLink" : "https://github.com/aellerton/japng"
},
{
"title" : "fxsvgimage",
"subtitle" : "Copyright (c) 2021, 2022, 2025 Hervé Girod.\nLicensed under the BSD 3-clause License.",
"externalLink" : "https://github.com/hervegirod/fxsvgimage"
},
{
"title": "Terracotta",
"subtitle": "Copyright © 2025 Burning_TNT.\nLicensed under the AGPL 3.0 License.",
+15 -39
View File
@@ -113,26 +113,28 @@
.memory-label {
}
.memory-status-bar {
-fx-opacity: 1;
.memory-used {
-fx-background-color: -monet-primary;
}
.memory-status-bar:disabled {
.memory-used:disabled {
-fx-opacity: 0.4;
}
.memory-status-bar .track {
-fx-background-color: -monet-secondary-container;
-fx-background-radius: 2;
-fx-background-insets: 0;
.memory-allocate {
-fx-background-color: -monet-primary-transparent-50;
}
.memory-status-bar .memory-used {
-fx-fill: -monet-primary;
.memory-allocate:disabled {
-fx-opacity: 0.4;
}
.memory-status-bar .memory-allocate {
-fx-fill: -monet-primary-transparent-50;
.memory-total {
-fx-background-color: -monet-surface-container;
}
.memory-total:disabled {
-fx-opacity: 0.4;
}
.update-label {
@@ -525,8 +527,7 @@
}
.jfx-dialog-layout {
-fx-background-radius: 4px;
-fx-padding: 24px 24px 16px 24px;
-fx-padding: 24.0px 24.0px 16.0px 24.0px;
-fx-background-color: -monet-surface-container-high;
-fx-text-fill: -monet-on-surface;
}
@@ -569,7 +570,6 @@
}
.task-executor-dialog-layout {
-fx-background-radius: 4px;
-fx-background-color: -monet-surface-container-high;
-fx-text-fill: -monet-on-surface;
}
@@ -1463,11 +1463,6 @@
-jfx-radius: 9;
}
.spinner-pane .spinner-pane-content {
-fx-padding: 10px;
-fx-spacing: 10px;
}
/*******************************************************************************
* *
* JFX Combo Box *
@@ -1570,21 +1565,6 @@
-fx-background-color: -monet-on-surface;
}
.custom-color-dialog .jfx-color-dialog-actions {
-fx-background-color: -monet-surface;
-fx-padding: 0.0 10.0 5.0 0.0;
-fx-alignment: center-right;
}
.custom-color-dialog .jfx-color-dialog-accept {
-fx-padding: 0.7em 0.8em;
}
.custom-color-dialog .jfx-color-dialog-cancel {
-fx-text-fill: -monet-on-surface-variant;
-fx-padding: 0.7em 0.8em;
}
/*******************************************************************************
* *
* Table View *
@@ -1943,8 +1923,4 @@
.line-toggle-button .jfx-toggle-button {
-fx-padding: 0;
}
.update-pane:active .subtitle-label {
-fx-text-fill: -monet-tertiary;
}
}
@@ -167,9 +167,6 @@ account.skin.upload.failed=Failed to upload skin.
account.skin.invalid_skin=Invalid skin file.
account.username=Username
addon.download.title.release=Minecraft %s
addon.download.title.snapshot=Minecraft %s (Snapshots)
archive.author=Author(s)
archive.date=Publish Date
archive.file.name=File Name
@@ -400,7 +397,7 @@ extension.ps1=Windows PowerShell Script
extension.sh=Shell Script
extension.command=macOS Shell Script
extension.datapack=Data Pack Archive
extension.datapack=Datapack Archive
extension.mod=Mod File
extension.modloader.installer=Mod Loader Installer
extension.resourcepack=Resource Pack Archive
@@ -458,8 +455,6 @@ fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher has provided
\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\
Click the link to navigate to the Microsoft Store and install the compatibility pack.
# 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.
file=File
@@ -707,7 +702,7 @@ game.version=Game Instance
help=Help
help.doc=Hello Minecraft! Launcher Documentation
help.detail=For data pack and modpack makers.
help.detail=For datapack and modpack makers.
input.email=The username must be an email address.
input.number=The input must be numbers.
@@ -997,7 +992,7 @@ modrinth.category.colored-lighting=Colored Lighting
modrinth.category.combat=Combat
modrinth.category.core-shaders=Core Shaders
modrinth.category.cursed=Cursed
modrinth.category.datapack=Data Pack
modrinth.category.datapack=Datapack
modrinth.category.decoration=Decoration
modrinth.category.economy=Economy
modrinth.category.entities=Entities
@@ -1140,11 +1135,11 @@ nbt.open.failed=Failed to open file
nbt.save.failed=Failed to save file
nbt.title=View File - %s
datapack=Data Packs
datapack=Datapacks
datapack.add=Add
datapack.add.title=Choose data pack archive you want to add
datapack.reload.toast=Minecraft is running. Use the /reload command to reload data packs
datapack.title=World [%s] - Data Packs
datapack.add.title=Choose datapack archive you want to add
datapack.reload.toast=Minecraft is running, please use the /reload command to reload the data pack
datapack.title=World [%s] - Datapacks
web.failed=Failed to load page
web.open_in_browser=Do you want to open this address in a browser:\n%s
@@ -1174,7 +1169,7 @@ world.duplicate.failed.empty_name=Name cannot be empty
world.duplicate.failed.invalid_name=Name contains invalid characters
world.duplicate.failed=Failed to duplicate the world
world.duplicate.success.toast=Successfully duplicated the world
world.datapack=Data Packs
world.datapack=Datapacks
world.datetime=Last played on %s
world.delete=Delete the World
world.delete.failed=Failed to delete world.\n%s
@@ -1388,9 +1383,8 @@ settings.advanced.renderer.zink.desc=Vulkan (Best performance, poor compatibilit
settings.advanced.server_ip=Server Address
settings.advanced.server_ip.prompt=Automatically join after launching the game
settings.advanced.unsupported_system_options=Settings not applicable to the current system
settings.advanced.use_native_glfw=Use System GLFW
settings.advanced.use_native_openal=Use System OpenAL
settings.advanced.linux_freebsd_only=Linux/FreeBSD Only
settings.advanced.use_native_glfw=[Linux/FreeBSD Only] Use System GLFW
settings.advanced.use_native_openal=[Linux/FreeBSD Only] Use System OpenAL
settings.advanced.workaround=Workaround
settings.advanced.workaround.warning=Workaround options are intended only for advanced users. Tweaking with these options may crash the game. Unless you know what you are doing, please do not edit these options.
settings.advanced.wrapper_launcher=Wrapper Command
@@ -1441,7 +1435,6 @@ settings.launcher.download.threads.auto=Automatically Determine
settings.launcher.download.threads.hint=Too many threads may cause your system to freeze, and your download speed may be affected by your ISP and download servers. It is not always the case that more threads increase your download speed.
settings.launcher.download_source=Download Source
settings.launcher.download_source.auto=Automatically Choose Download Sources
settings.launcher.default_addon_source=Default Addon Source
settings.launcher.enable_game_list=Show instance list in homepage
settings.launcher.font=Font
settings.launcher.font.anti_aliasing=Anti-aliasing
@@ -1590,7 +1583,7 @@ update=Update
update.accept=Update
update.changelog=Changelog
update.channel.dev=Beta
update.channel.dev.hint=You are currently using a Beta channel build of the launcher. While it may include some extra features, it is also sometimes less stable than the Stable channel builds.\n\
update.channel.dev.hint=You are currently using a Beta channel build of the launcher. While it may include some extra features, it is also sometimes less stable than the Release channel builds.\n\
\n\
If you encounter any bugs or problems, please submit feedback via the channels provided on the <a href="hmcl://settings/feedback">Feedback</a> page.\n\
\n\
@@ -1603,7 +1596,7 @@ update.channel.nightly.hint=You are currently using a Nightly channel build of t
\n\
Follow <a href="https://space.bilibili.com/1445341">@huanghongxun</a> on Bilibili to stay up to date on important HMCL news, or <a href="https://space.bilibili.com/20314891">@Glavo</a> to learn about HMCL development progress.
update.channel.nightly.title=Nightly Channel Notice
update.channel.stable=Stable
update.channel.stable=Release
update.checking=Checking for updates
update.disable_auto_show_update_dialog=Do not show update dialog automatically
update.disable_auto_show_update_dialog.subtitle=Enable this option to prevent HMCL from automatically showing the update dialog.
@@ -1612,8 +1605,7 @@ update.found=Update Available!
update.newest_version=Latest version: %s
update.bubble.title=Update Available: %s
update.bubble.subtitle=Click here to update
update.note.stable=Suitable for users who prioritize software stability.\nNew features are added to the stable version only after thorough testing.
update.note.dev=Suitable for users who want to experience new features first.\nThe development version includes the latest features and bug fixes,\nbut may also have more issues due to insufficient testing.
update.note=Beta and Nightly channels may have more features or fixes, but they also come with more potential problems.
update.latest=This is the latest version
update.no_browser=Cannot open in system browser. But we copied the link to your clipboard, and you can open it manually.
update.tooltip=Update
@@ -1375,9 +1375,8 @@ settings.advanced.renderer.zink.desc=Vulkan (أفضل أداء، توافق ضع
settings.advanced.server_ip=عنوان الخادم
settings.advanced.server_ip.prompt=الانضمام تلقائياً بعد تشغيل اللعبة
settings.advanced.unsupported_system_options=إعدادات غير مطبّقة على النظام الحالي
settings.advanced.use_native_glfw=استخدام GLFW النظام
settings.advanced.use_native_openal=استخدام OpenAL النظام
settings.advanced.linux_freebsd_only=Linux/FreeBSD فقط
settings.advanced.use_native_glfw=[Linux/FreeBSD فقط] استخدام GLFW النظام
settings.advanced.use_native_openal=[Linux/FreeBSD فقط] استخدام OpenAL النظام
settings.advanced.workaround=حلول بديلة
settings.advanced.workaround.warning=خيارات الحلول البديلة مخصصة للمستخدمين المتقدمين فقط. قد يتسبب تعديل هذه الخيارات في تعطل اللعبة. يُرجى عدم تعديل هذه الخيارات إلا إذا كنت تعرف ما تفعله.
settings.advanced.wrapper_launcher=أمر التغليف
@@ -1593,6 +1592,7 @@ update.found=تحديث متاح!
update.newest_version=أحدث إصدار: %s
update.bubble.title=تحديث متاح: %s
update.bubble.subtitle=انقر هنا للتحديث
update.note=قد تحتوي القنوات التجريبية والليلية على مزيد من الميزات والإصلاحات، لكنها تأتي أيضاً بمزيد من المشاكل المحتملة.
update.latest=هذا هو أحدث إصدار
update.no_browser=لا يمكن الفتح في متصفح النظام. لكننا نسخنا الرابط إلى حافظتك، ويمكنك فتحه يدوياً.
update.tooltip=تحديث
@@ -1279,9 +1279,8 @@ settings.advanced.renderer.zink.desc=Vulkan (Máximo rendimiento, baja compatibi
settings.advanced.server_ip=Dirección del servidor
settings.advanced.server_ip.prompt=Entrar automáticamente después de ejecutar el juego
settings.advanced.unsupported_system_options=Configuración no aplicable al sistema actual
settings.advanced.use_native_glfw=Utilizar GLFW nativo
settings.advanced.use_native_openal=Utilizar OpenAL nativo
settings.advanced.linux_freebsd_only=Sólo Linux/FreeBSD
settings.advanced.use_native_glfw=[Sólo Linux/FreeBSD] Utilizar GLFW nativo
settings.advanced.use_native_openal=[Sólo Linux/FreeBSD] Utilizar OpenAL nativo
settings.advanced.workaround=Métodos alternativos
settings.advanced.workaround.warning=Éstas opciones están pensadas sólo para usuarios expertos. Jugar con estas opciones puede romper el juego. A menos que sepas lo que estás haciendo, por favor no modifiques estas opciones.
settings.advanced.wrapper_launcher=Comando Wrapper
@@ -1402,6 +1401,7 @@ update.found=¡Actualización disponible!
update.newest_version=Última versión: %s
update.bubble.title=Actualización disponible: %s
update.bubble.subtitle=Haga clic aquí para actualizar
update.note=Advertencia: Las versiones beta y las versiones nocturnas pueden tener más funciones o correcciones, pero también tienen más problemas potenciales.
update.latest=Esta es la última versión.
update.no_browser=No se puede abrir en el navegador del sistema. Pero, hemos copiado el enlace a su portapapeles y puede abrirlo manualmente.
update.tooltip=Actualización
@@ -802,9 +802,8 @@ settings.advanced.renderer.zink.desc=Vulkan (最高パフォーマンス、低
settings.advanced.server_ip=サーバーアドレス
settings.advanced.server_ip.prompt=ゲームの起動時にサーバーに参加する
settings.advanced.unsupported_system_options=サポートされていないシステムオプション
settings.advanced.use_native_glfw=システムGLFWを使用する
settings.advanced.use_native_openal=システムOpenALを使用する
settings.advanced.linux_freebsd_only=Linux/FreeBSDのみ
settings.advanced.use_native_glfw=[Linux/FreeBSDのみ]システムGLFWを使用する
settings.advanced.use_native_openal=[Linux/FreeBSDのみ]システムOpenALを使用する
settings.advanced.workaround=デバッグ用オプション
settings.advanced.workaround.warning=デバッグオプションはプロフェッショナルのみ使用可能です。デバッグオプションにより、ゲームが起動しない場合があります。これらのオプションは、ご自分が何をしているのかが分からない限り、変更しないでください。
settings.advanced.wrapper_launcher=パッキングオーダー
@@ -912,6 +911,7 @@ update.found=アップデートが利用可能です!
update.newest_version=最新バージョン:%s
update.bubble.title=利用可能な更新:%s
update.bubble.subtitle=更新するにはここをクリックしてください
update.note=警告:ベータ版には、より多くの機能とバグ修正が含まれている可能性がありますが、エラーが発生する可能性もあります。
update.latest=これは最新バージョンです。
update.no_browser=ブラウザを開くことができません。リンクがクリップボードにコピーされました。ブラウザのアドレスバーに貼り付けて更新します。
update.tooltip=更新
@@ -1095,9 +1095,8 @@ settings.advanced.renderer.zink.desc=Vulkan (效至,所適差)
settings.advanced.server_ip=伺服器之址
settings.advanced.server_ip.prompt=本之址,啟訖徑入是伺服器
settings.advanced.unsupported_system_options=不逮今之械網所選
settings.advanced.use_native_glfw=用 GLFW
settings.advanced.use_native_openal=用開聲庫
settings.advanced.linux_freebsd_only=惟磷若/FreeBSD
settings.advanced.use_native_glfw=[惟磷若/FreeBSD] 用 GLFW
settings.advanced.use_native_openal=[惟磷若/FreeBSD] 用開聲庫
settings.advanced.workaround=勘誤置設
settings.advanced.workaround.warning=勘誤置設者,蓋專者之所行也。苟妄改之,戲或弗啟。微其之知,無復改焉!
settings.advanced.wrapper_launcher=包裝之令
@@ -1227,6 +1226,7 @@ update.found=尋得新者
update.newest_version=至新:%s
update.bubble.title=尋得更新:%s
update.bubble.subtitle=是擊以晉
update.note=开發版與預版,虽增功、修阙,然亦恐生他患。
update.latest=當版至新
update.no_browser=無啟瀏覽器。址固鈔訖,君可自粘之以啟頁。
update.tooltip=迭更
@@ -1270,9 +1270,8 @@ settings.advanced.renderer.zink.desc=Vulkan (Лучшая производите
settings.advanced.server_ip=Адрес сервера
settings.advanced.server_ip.prompt=Присоединяться к серверу при запуске игры
settings.advanced.unsupported_system_options=Настройки, не применимые к текущей системе
settings.advanced.use_native_glfw=Использовать системный GLFW
settings.advanced.use_native_openal=Использовать системный OpenAL
settings.advanced.linux_freebsd_only=Только для Linux/FreeBSD
settings.advanced.use_native_glfw=[Только для Linux/FreeBSD] Использовать системный GLFW
settings.advanced.use_native_openal=[Только для Linux/FreeBSD] Использовать системный OpenAL
settings.advanced.workaround=Обходные пути
settings.advanced.workaround.warning=Варианты обхода предназначены только для опытных пользователей. Изменение этих параметров может привести к вылету игры. Если не знаете, что делаете, то не изменяйте эти параметры.
settings.advanced.wrapper_launcher=Команда-оболочка
@@ -1392,6 +1391,7 @@ update.found=Доступно обновление!
update.newest_version=Последняя версия: %s
update.bubble.title=Доступно обновление: %s
update.bubble.subtitle=Кликните здесь для обновления
update.note=Предупреждение: Бета-версии могут иметь больше функциональных возможностей и исправлений ошибок, но также и больше возможных ошибок.
update.latest=Это последняя версия.
update.no_browser=Не удалось открыть браузер. Ссылка была скопирована в буфер обмена. Вставьте его в адресную строку браузера для обновления.
update.tooltip=Обновить
@@ -1217,9 +1217,8 @@ settings.advanced.renderer.zink.desc=Vulkan (Найкраща продуктив
settings.advanced.server_ip=Адреса сервера
settings.advanced.server_ip.prompt=Автоматично приєднатися після запуску гри
settings.advanced.unsupported_system_options=Налаштування, що не застосовуються до поточної системи
settings.advanced.use_native_glfw=Використовувати системний GLFW
settings.advanced.use_native_openal=Використовувати системний OpenAL
settings.advanced.linux_freebsd_only=Лише Linux/FreeBSD
settings.advanced.use_native_glfw=[Лише Linux/FreeBSD] Використовувати системний GLFW
settings.advanced.use_native_openal=[Лише Linux/FreeBSD] Використовувати системний OpenAL
settings.advanced.workaround=Обхідні шляхи
settings.advanced.workaround.warning=Опції обхідних шляхів призначені лише для досвідчених користувачів. Налаштування цих опцій може призвести до збою гри. Якщо ви не знаєте, що робите, не редагуйте ці опції.
settings.advanced.wrapper_launcher=Команда обгортки
@@ -1343,6 +1342,7 @@ update.found=Доступне оновлення!
update.newest_version=Остання версія: %s
update.bubble.title=Доступне оновлення: %s
update.bubble.subtitle=Натисніть тут, щоб оновити
update.note=Канали Бета та Нічний можуть мати більше функцій або виправлень, але вони також мають більше потенційних проблем.
update.latest=Це остання версія
update.no_browser=Не вдається відкрити в системному браузері. Але ми скопіювали посилання до вашого буфера обміну, і ви можете відкрити його вручну.
update.tooltip=Оновити
@@ -164,9 +164,6 @@ account.skin.upload.failed=外觀上傳失敗
account.skin.invalid_skin=無法識別的外觀檔案
account.username=使用者名稱
addon.download.title.release=Minecraft %s
addon.download.title.snapshot=Minecraft %s (快照)
archive.author=作者
archive.date=發布日期
archive.file.name=檔案名稱
@@ -421,8 +418,6 @@ fatal.unsupported_platform=Minecraft 尚未對您的平臺提供完善支援,
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按下「確定」以繼續,但請注意,此操作可能會引發一些問題。
file=檔案
@@ -1175,9 +1170,8 @@ settings.advanced.renderer.zink.desc=Vulkan (效能最好,相容性較差)
settings.advanced.server_ip=伺服器位址
settings.advanced.server_ip.prompt=預設,啟動遊戲後直接進入對應伺服器
settings.advanced.unsupported_system_options=不適用於目前系統的選項
settings.advanced.use_native_glfw=使用系統 GLFW
settings.advanced.use_native_openal=使用系統 OpenAL
settings.advanced.linux_freebsd_only=僅限 Linux/FreeBSD
settings.advanced.use_native_glfw=[僅限 Linux/FreeBSD] 使用系統 GLFW
settings.advanced.use_native_openal=[僅限 Linux/FreeBSD] 使用系統 OpenAL
settings.advanced.workaround=除錯選項
settings.advanced.workaround.warning=除錯選項僅提供給專業玩家使用。修改除錯選項可能會導致遊戲無法啟動。除非你知道你在做什麼,否則請不要修改這些選項。
settings.advanced.wrapper_launcher=前置指令
@@ -1226,7 +1220,6 @@ settings.launcher.download.threads.auto=自動選取執行緒數
settings.launcher.download.threads.hint=執行緒數過高可能導致系統卡頓。你的下載速度會受到網際網路運營商、下載來源伺服器等方面的影響。調高下載執行緒數不一定能大幅提升總下載速度。
settings.launcher.download_source=下載來源
settings.launcher.download_source.auto=自動選取下載來源
settings.launcher.default_addon_source=遊戲內容預設下載來源
settings.launcher.enable_game_list=在首頁內顯示遊戲清單
settings.launcher.font=字體
settings.launcher.font.anti_aliasing=反鋸齒
@@ -1393,8 +1386,7 @@ update.found=發現到更新
update.newest_version=最新版本為:%s
update.bubble.title=發現更新:%s
update.bubble.subtitle=點選此處進行升級
update.note.stable=適合優先追求軟體穩定性的使用者使用。\n新功能在經過充分測試後才會被添加到穩定版中。
update.note.dev=適合希望優先體驗新功能的使用者使用。\n開發版會包含最新功能和錯誤修復,但未經充分測試,可能會存在更多問題。
update.note=開發版與預覽版包含更多的功能以及錯誤修復,但也可能會包含其他的問題。
update.latest=目前版本為最新版本
update.no_browser=無法開啟瀏覽器。網址已經複製到剪貼簿了,你可以手動複製網址開啟頁面。
update.tooltip=更新
@@ -166,9 +166,6 @@ account.skin.upload.failed=皮肤上传失败
account.skin.invalid_skin=无法识别的皮肤文件
account.username=用户名
addon.download.title.release=Minecraft %s
addon.download.title.snapshot=Minecraft %s (快照)
archive.author=作者
archive.date=发布日期
archive.file.name=文件名
@@ -425,8 +422,6 @@ fatal.unsupported_platform=Minecraft 尚未对你的平台提供完善支持,
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按下“确定”以继续,但请注意,此操作可能会引发一些问题。
file=文件
@@ -1180,9 +1175,8 @@ settings.advanced.renderer.zink.desc=Vulkan (性能最好,兼容性较差)
settings.advanced.server_ip=服务器地址
settings.advanced.server_ip.prompt=默认,启动游戏后可以直接进入对应服务器
settings.advanced.unsupported_system_options=不适用于当前系统的选项
settings.advanced.use_native_glfw=使用系统 GLFW
settings.advanced.use_native_openal=使用系统 OpenAL
settings.advanced.linux_freebsd_only=仅 Linux/FreeBSD
settings.advanced.use_native_glfw=[仅 Linux/FreeBSD] 使用系统 GLFW
settings.advanced.use_native_openal=[仅 Linux/FreeBSD] 使用系统 OpenAL
settings.advanced.workaround=调试选项
settings.advanced.workaround.warning=调试选项仅提供给专业玩家使用。调试选项可能会导致游戏无法启动。除非你知道你在做什么,否则请不要修改这些选项!
settings.advanced.wrapper_launcher=包装命令
@@ -1231,7 +1225,6 @@ settings.launcher.download.threads.auto=自动选择线程数
settings.launcher.download.threads.hint=线程数过高可能导致系统卡顿。你的下载速度会受到互联网运营商、下载源服务器等方面的影响。调高下载线程数不一定能大幅提升总下载速度。
settings.launcher.download_source=下载源
settings.launcher.download_source.auto=自动选择下载源
settings.launcher.default_addon_source=游戏内容默认下载源
settings.launcher.enable_game_list=在主页内显示版本列表
settings.launcher.font=字体
settings.launcher.font.anti_aliasing=抗锯齿
@@ -1398,8 +1391,7 @@ update.found=发现更新
update.newest_version=最新版本为:%s
update.bubble.title=发现更新:%s
update.bubble.subtitle=点击此处进行升级
update.note.stable=适合优先追求软件稳定性的用户使用。\n新功能在经过充分测试后才会被添加到稳定版中。
update.note.dev=适合希望优先体验新功能的用户使用。\n包含最新功能和错误修复,但未经充分测试,可能会存在更多问题。
update.note=开发版与预览版包含更多的功能以及错误修复,但也可能会包含其他的问题。
update.latest=当前版本为最新版本
update.no_browser=无法打开浏览器。网址已经复制到剪贴板,你可以手动粘贴网址打开页面。
update.tooltip=更新
@@ -1,20 +1,3 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util;
import javafx.collections.FXCollections;
+1 -2
View File
@@ -18,15 +18,14 @@ dependencies {
api(libs.gson)
api(libs.toml)
api(libs.xz)
api(libs.lz4)
api(libs.fx.gson)
api(libs.constant.pool.scanner)
api(libs.opennbt)
api(libs.nanohttpd)
api(libs.jsoup)
api(libs.chardet)
api(libs.jna)
api(libs.pci.ids)
api(libs.hello.nbt)
compileOnlyApi(libs.jetbrains.annotations)
@@ -1,20 +1,3 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.download.neoforge;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
@@ -1,26 +1,10 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.download.neoforge;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonSerializable;
import java.util.Collections;
import java.util.List;
@@ -75,16 +59,38 @@ public final class NeoForgeOfficialVersionList extends VersionList<NeoForgeRemot
String mcVersion;
try {
int si1 = version.indexOf('.'), si2 = version.indexOf('.', version.indexOf('.') + 1);
int si1 = version.indexOf('.');
int si2 = version.indexOf('.', si1 + 1);
if (si1 < 0 || si2 < 0) {
LOG.warning("Unsupported NeoForge version: " + version);
continue;
}
int majorVersion = Integer.parseInt(version.substring(0, si1));
if (majorVersion == 0) { // Snapshot version.
mcVersion = version.substring(si1 + 1, si2);
} else {
String ver = version.substring(0, Integer.parseInt(version.substring(si1 + 1, si2)) == 0 ? si1 : si2);
if (majorVersion >= 26) {
int si3 = version.indexOf('.', si2 + 1);
if (si3 < 0) {
LOG.warning("Unsupported NeoForge version: " + version);
continue;
}
String ver = Integer.parseInt(version.substring(si2 + 1, si3)) == 0
? version.substring(0, si2)
: version.substring(0, si3);
int separator = version.indexOf('+');
mcVersion = separator < 0 ? ver : ver + "-" + version.substring(separator + 1);
if (separator < 0)
mcVersion = ver;
else
mcVersion = ver + "-" + version.substring(separator + 1);
} else {
String ver = Integer.parseInt(version.substring(si1 + 1, si2)) == 0
? version.substring(0, si1)
: version.substring(0, si2);
mcVersion = "1." + ver;
}
}
@@ -106,14 +112,7 @@ public final class NeoForgeOfficialVersionList extends VersionList<NeoForgeRemot
});
}
private static final class OfficialAPIResult {
private final boolean isSnapshot;
private final List<String> versions;
public OfficialAPIResult(boolean isSnapshot, List<String> versions) {
this.isSnapshot = isSnapshot;
this.versions = versions;
}
@JsonSerializable
private record OfficialAPIResult(boolean isSnapshot, List<String> versions) {
}
}
@@ -1,20 +1,3 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.download.neoforge;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
@@ -15,6 +15,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
import com.sun.tools.attach.AttachNotSupportedException;
@@ -1,20 +1,3 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
public enum NativesDirectoryType {
@@ -1,20 +1,3 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
public enum Renderer {
@@ -17,9 +17,9 @@
*/
package org.jackhuang.hmcl.game;
import com.github.steveice10.opennbt.NBTIO;
import com.github.steveice10.opennbt.tag.builtin.*;
import javafx.scene.image.Image;
import org.glavo.nbt.io.NBTCodec;
import org.glavo.nbt.tag.*;
import org.jackhuang.hmcl.util.io.*;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
@@ -34,7 +34,9 @@ import java.nio.channels.OverlappingFileLockException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.List;
import java.util.UUID;
import java.util.stream.Stream;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -46,7 +48,6 @@ public final class World {
private Image icon;
private CompoundTag levelData;
private CompoundTag dataTag;
private Path levelDataPath;
private CompoundTag worldGenSettingsDataBackingTag; // Use for writing back to the file
@@ -59,64 +60,10 @@ public final class World {
public World(Path file) throws IOException {
this.file = file;
if (Files.isDirectory(file)) {
fileName = FileUtils.getName(this.file);
Path levelDatPath = this.file.resolve("level.dat");
if (!Files.exists(levelDatPath)) { // version 20w14infinite
levelDatPath = this.file.resolve("special_level.dat");
}
if (!Files.exists(levelDatPath)) {
throw new IOException("Not a valid world directory since level.dat or special_level.dat cannot be found.");
}
this.levelDataPath = levelDatPath;
loadAndCheckWorldData();
Path iconFile = this.file.resolve("icon.png");
if (Files.isRegularFile(iconFile)) {
try (InputStream inputStream = Files.newInputStream(iconFile)) {
icon = new Image(inputStream, 64, 64, true, false);
if (icon.isError())
throw icon.getException();
} catch (Exception e) {
LOG.warning("Failed to load world icon", e);
}
}
} else if (Files.isRegularFile(file))
try (FileSystem fs = CompressingUtils.readonly(this.file).setAutoDetectEncoding(true).build()) {
Path root;
if (Files.isRegularFile(fs.getPath("/level.dat"))) {
root = fs.getPath("/");
fileName = FileUtils.getName(this.file);
} else {
List<Path> files = Files.list(fs.getPath("/")).toList();
if (files.size() != 1 || !Files.isDirectory(files.get(0))) {
throw new IOException("Not a valid world zip file");
}
root = files.get(0);
fileName = FileUtils.getName(root);
}
Path levelDat = root.resolve("level.dat");
if (!Files.exists(levelDat)) { //version 20w14infinite
levelDat = root.resolve("special_level.dat");
}
if (!Files.exists(levelDat)) {
throw new IOException("Not a valid world zip file since level.dat or special_level.dat cannot be found.");
}
loadAndCheckLevelData(levelDat);
Path iconFile = root.resolve("icon.png");
if (Files.isRegularFile(iconFile)) {
try (InputStream inputStream = Files.newInputStream(iconFile)) {
icon = new Image(inputStream, 64, 64, true, false);
if (icon.isError())
throw icon.getException();
} catch (Exception e) {
LOG.warning("Failed to load world icon", e);
}
}
}
if (Files.isDirectory(file))
loadFromDirectory();
else if (Files.isRegularFile(file))
loadFromZip();
else
throw new IOException("Path " + file + " cannot be recognized as a Minecraft world");
}
@@ -130,11 +77,9 @@ public final class World {
}
public String getWorldName() {
if (levelData.get("Data") instanceof CompoundTag data
&& data.get("LevelName") instanceof StringTag levelNameTag)
return levelNameTag.get();
else
return "";
CompoundTag data = levelData.get("Data");
StringTag levelNameTag = data.get("LevelName");
return levelNameTag.getValue();
}
public void setWorldName(String worldName) throws IOException {
@@ -161,11 +106,9 @@ public final class World {
}
public long getLastPlayed() {
if (dataTag.get("LastPlayed") instanceof LongTag lastPlayedTag) {
return lastPlayedTag.get();
} else {
return 0L;
}
CompoundTag data = levelData.get("Data");
LongTag lastPlayedTag = data.get("LastPlayed");
return lastPlayedTag.getValue();
}
public @Nullable GameVersionNumber getGameVersion() {
@@ -179,20 +122,21 @@ public final class World {
public @Nullable Long getSeed() {
// Valid after 1.16(20w20a)
if (normalizedWorldGenSettingsData != null
&& normalizedWorldGenSettingsData.get("seed") instanceof LongTag seedTag) {
if (normalizedWorldGenSettingsData != null && normalizedWorldGenSettingsData.get("seed") instanceof LongTag seedTag) {
return seedTag.getValue();
}
// Valid before 1.16(20w20a)
if (dataTag.get("RandomSeed") instanceof LongTag seedTag) {
if (levelData.get("Data") instanceof CompoundTag data && data.get("RandomSeed") instanceof LongTag seedTag) {
return seedTag.getValue();
}
return null;
}
public boolean isLargeBiomes() {
CompoundTag data = levelData.get("Data");
// Valid before 1.16(20w20a)
if (dataTag.get("generatorName") instanceof StringTag generatorNameTag) {
if (data.get("generatorName") instanceof StringTag generatorNameTag) {
return "largeBiomes".equals(generatorNameTag.getValue());
}
// Unified handling of logic after version 1.16
@@ -203,11 +147,11 @@ public final class World {
// Valid between 1.16(20w20a) and 1.18(21w37a)
if (generatorTag.get("biome_source") instanceof CompoundTag biomeSourceTag
&& biomeSourceTag.get("large_biomes") instanceof ByteTag largeBiomesTag) {
return largeBiomesTag.get() == (byte) 1;
return largeBiomesTag.getValue() == (byte) 1;
}
// Valid after 1.18(21w37a)
else if (generatorTag.get("settings") instanceof StringTag settingsTag) {
return "minecraft:large_biomes".equals(settingsTag.get());
return "minecraft:large_biomes".equals(settingsTag.getValue());
}
}
}
@@ -222,7 +166,7 @@ public final class World {
return isLocked(getSessionLockFile());
}
public boolean supportDataPacks() {
public boolean supportDatapacks() {
return getGameVersion() != null && getGameVersion().isAtLeast("1.13", "17w43a");
}
@@ -234,14 +178,79 @@ public final class World {
return gameVersionNumber != null && gameVersionNumber.isAtLeast("1.20", "23w14a");
}
private void loadFromDirectory() throws IOException {
fileName = FileUtils.getName(file);
Path levelDat = file.resolve("level.dat");
if (!Files.exists(levelDat)) { // version 20w14infinite
levelDat = file.resolve("special_level.dat");
}
if (!Files.exists(levelDat)) {
throw new IOException("Not a valid world directory since level.dat or special_level.dat cannot be found.");
}
this.levelDataPath = levelDat;
loadAndCheckWorldData();
Path iconFile = file.resolve("icon.png");
if (Files.isRegularFile(iconFile)) {
try (InputStream inputStream = Files.newInputStream(iconFile)) {
icon = new Image(inputStream, 64, 64, true, false);
if (icon.isError())
throw icon.getException();
} catch (Exception e) {
LOG.warning("Failed to load world icon", e);
}
}
}
private void loadFromZipImpl(Path root) throws IOException {
Path levelDat = root.resolve("level.dat");
if (!Files.exists(levelDat)) { //version 20w14infinite
levelDat = root.resolve("special_level.dat");
}
if (!Files.exists(levelDat)) {
throw new IOException("Not a valid world zip file since level.dat or special_level.dat cannot be found.");
}
loadAndCheckLevelData(levelDat);
Path iconFile = root.resolve("icon.png");
if (Files.isRegularFile(iconFile)) {
try (InputStream inputStream = Files.newInputStream(iconFile)) {
icon = new Image(inputStream, 64, 64, true, false);
if (icon.isError())
throw icon.getException();
} catch (Exception e) {
LOG.warning("Failed to load world icon", e);
}
}
}
private void loadFromZip() throws IOException {
try (FileSystem fs = CompressingUtils.readonly(file).setAutoDetectEncoding(true).build()) {
Path levelDatPath = fs.getPath("/level.dat");
if (Files.isRegularFile(levelDatPath)) {
fileName = FileUtils.getName(file);
loadFromZipImpl(fs.getPath("/"));
return;
}
try (Stream<Path> stream = Files.list(fs.getPath("/"))) {
Path root = stream.filter(Files::isDirectory).findAny()
.orElseThrow(() -> new IOException("Not a valid world zip file"));
fileName = FileUtils.getName(root);
loadFromZipImpl(root);
}
}
}
private void loadAndCheckWorldData() throws IOException {
loadAndCheckLevelData(levelDataPath);
loadOtherData();
}
private void loadAndCheckLevelData(Path levelDat) throws IOException {
this.levelData = NBTCodec.of().readTag(levelDat, TagType.COMPOUND);
if (!(levelData.get("Data") instanceof CompoundTag data))
this.levelData = readTag(levelDat);
CompoundTag data = levelData.get("Data");
if (data == null)
throw new IOException("level.dat missing Data");
if (!(data.get("LevelName") instanceof StringTag))
@@ -249,7 +258,6 @@ public final class World {
if (!(data.get("LastPlayed") instanceof LongTag))
throw new IOException("level.dat missing LastPlayed");
this.dataTag = data;
}
private void loadOtherData() throws IOException {
@@ -259,7 +267,7 @@ public final class World {
if (data.get("WorldGenSettings") instanceof CompoundTag worldGenSettingsTag) {
setWorldGenSettingsData(null, worldGenSettingsTag, worldGenSettingsTag);
} else if (Files.isRegularFile(worldGenSettingsDatPath)) {
CompoundTag raw = NBTCodec.of().readTag(worldGenSettingsDatPath, TagType.COMPOUND);
CompoundTag raw = readTag(worldGenSettingsDatPath);
if (raw.get("data") instanceof CompoundTag compoundTag) {
setWorldGenSettingsData(worldGenSettingsDatPath, raw, compoundTag);
} else {
@@ -271,11 +279,14 @@ public final class World {
if (data.get("Player") instanceof CompoundTag playerTag) {
setPlayerData(null, playerTag);
} else if (data.get("singleplayer_uuid") instanceof IntArrayTag uuidTag && uuidTag.isUUID()) {
String playerUUID = uuidTag.getUUID().toString();
} else if (data.get("singleplayer_uuid") instanceof IntArrayTag uuidTag && uuidTag.getValue().length == 4) {
int[] uuidValue = uuidTag.getValue();
long mostSigBits = ((long) uuidValue[0] << 32) | (uuidValue[1] & 0xFFFFFFFFL);
long leastSigBits = ((long) uuidValue[2] << 32) | (uuidValue[3] & 0xFFFFFFFFL);
String playerUUID = new UUID(mostSigBits, leastSigBits).toString();
Path playerDatPath = file.resolve("players/data/" + playerUUID + ".dat");
if (Files.exists(playerDatPath)) {
setPlayerData(playerDatPath, NBTCodec.of().readTag(playerDatPath, TagType.COMPOUND));
setPlayerData(playerDatPath, readTag(playerDatPath));
} else {
setPlayerData(null, null);
}
@@ -306,7 +317,8 @@ public final class World {
throw new IOException("Not a valid world directory");
// Change the name recorded in level.dat
dataTag.setString("LevelName", newName);
CompoundTag data = levelData.get("Data");
data.put(new StringTag("LevelName", newName));
writeLevelData();
// then change the folder's name
@@ -421,11 +433,19 @@ public final class World {
writeTag(levelData, levelDataPath);
}
private CompoundTag readTag(Path path) throws IOException {
try (InputStream is = new GZIPInputStream(Files.newInputStream(path))) {
Tag tag = NBTIO.readTag(is);
if (tag instanceof CompoundTag compoundTag) return compoundTag;
throw new IOException("NBT file malformed: " + path);
}
}
private void writeTag(CompoundTag nbt, Path path) throws IOException {
if (!Files.isDirectory(file)) throw new IOException("Not a valid world directory");
FileUtils.saveSafely(path, os -> {
try (OutputStream gos = new GZIPOutputStream(os)) {
NBTCodec.of().writeTag(gos, nbt);
NBTIO.writeTag(gos, nbt);
}
});
}
@@ -1,20 +1,3 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.launch;
public class CommandTooLongException extends RuntimeException {
@@ -1,20 +1,3 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.launch;
public final class ExecutionPolicyLimitException extends RuntimeException {
@@ -39,14 +39,14 @@ import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class DataPack {
public class Datapack {
private static final String DISABLED_EXT = "disabled";
private static final String ZIP_EXT = "zip";
private final Path path;
private final ObservableList<Pack> packs = FXCollections.observableArrayList();
public DataPack(Path path) {
public Datapack(Path path) {
this.path = path;
}
@@ -58,14 +58,14 @@ public class DataPack {
return packs;
}
public static void installPack(Path sourceDataPackPath, Path targetDataPackDirectory, GameVersionNumber gameVersionNumber) throws IOException {
public static void installPack(Path sourceDatapackPath, Path targetDatapackDirectory, GameVersionNumber gameVersionNumber) throws IOException {
boolean containsMultiplePacks;
Set<String> packs = new HashSet<>();
try (FileSystem fs = CompressingUtils.readonly(sourceDataPackPath).setAutoDetectEncoding(true).build()) {
Path dataPacks = fs.getPath("datapacks");
try (FileSystem fs = CompressingUtils.readonly(sourceDatapackPath).setAutoDetectEncoding(true).build()) {
Path datapacks = fs.getPath("datapacks");
Path mcmeta = fs.getPath("pack.mcmeta");
if (Files.exists(dataPacks)) {
if (Files.exists(datapacks)) {
containsMultiplePacks = true;
} else if (Files.exists(mcmeta)) {
containsMultiplePacks = false;
@@ -74,14 +74,14 @@ public class DataPack {
}
if (containsMultiplePacks) {
try (Stream<Path> s = Files.list(dataPacks)) {
try (Stream<Path> s = Files.list(datapacks)) {
packs = s.map(FileUtils::getNameWithoutExtension).collect(Collectors.toSet());
}
} else {
packs.add(FileUtils.getNameWithoutExtension(sourceDataPackPath));
packs.add(FileUtils.getNameWithoutExtension(sourceDatapackPath));
}
try (DirectoryStream<Path> stream = Files.newDirectoryStream(targetDataPackDirectory)) {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(targetDatapackDirectory)) {
for (Path dir : stream) {
String packName = FileUtils.getName(dir);
if (FileUtils.getExtension(dir).equals(DISABLED_EXT)) {
@@ -100,9 +100,9 @@ public class DataPack {
}
if (!containsMultiplePacks) {
FileUtils.copyFile(sourceDataPackPath, targetDataPackDirectory.resolve(FileUtils.getName(sourceDataPackPath)));
FileUtils.copyFile(sourceDatapackPath, targetDatapackDirectory.resolve(FileUtils.getName(sourceDatapackPath)));
} else {
new Unzipper(sourceDataPackPath, targetDataPackDirectory)
new Unzipper(sourceDatapackPath, targetDatapackDirectory)
.setReplaceExistentFile(true)
.setSubDirectory("/datapacks/")
.unzip();
@@ -113,14 +113,14 @@ public class DataPack {
&& gameVersionNumber.compareTo("26.1-snapshot-6") >= 0;
if (useNewResourcePath) {
Files.createDirectories(targetDataPackDirectory.getParent().resolve("resourcepacks"));
targetResourceZipPath = targetDataPackDirectory.getParent().resolve("resourcepacks/resources.zip");
Files.createDirectories(targetDatapackDirectory.getParent().resolve("resourcepacks"));
targetResourceZipPath = targetDatapackDirectory.getParent().resolve("resourcepacks/resources.zip");
} else {
targetResourceZipPath = targetDataPackDirectory.getParent().resolve("resources.zip");
targetResourceZipPath = targetDatapackDirectory.getParent().resolve("resources.zip");
}
try (FileSystem outputResourcesZipFS = CompressingUtils.createWritableZipFileSystem(targetResourceZipPath);
FileSystem inputPackZipFS = CompressingUtils.createReadOnlyZipFileSystem(sourceDataPackPath)) {
FileSystem inputPackZipFS = CompressingUtils.createReadOnlyZipFileSystem(sourceDatapackPath)) {
Path resourcesZip = inputPackZipFS.getPath("resources.zip");
if (Files.isRegularFile(resourcesZip)) {
Path tempResourcesFile = Files.createTempFile("hmcl", ".zip");
@@ -229,14 +229,14 @@ public class DataPack {
}
}
private Optional<Pack> parsePack(Path dataPackPath, boolean isDirectory, String name, Path mcmetaPath) {
private Optional<Pack> parsePack(Path datapackPath, boolean isDirectory, String name, Path mcmetaPath) {
try {
PackMcMeta mcMeta = JsonUtils.fromNonNullJson(Files.readString(mcmetaPath), PackMcMeta.class);
return Optional.of(new Pack(dataPackPath, isDirectory, name, mcMeta.pack().description(), this));
return Optional.of(new Pack(datapackPath, isDirectory, name, mcMeta.pack().description(), this));
} catch (JsonParseException e) {
LOG.warning("Invalid pack.mcmeta format in " + dataPackPath, e);
LOG.warning("Invalid pack.mcmeta format in " + datapackPath, e);
} catch (IOException e) {
LOG.warning("IO error reading " + dataPackPath, e);
LOG.warning("IO error reading " + datapackPath, e);
}
return Optional.empty();
}
@@ -248,14 +248,14 @@ public class DataPack {
private final BooleanProperty activeProperty;
private final String id;
private final LocalModFile.Description description;
private final DataPack parentDataPack;
private final Datapack parentDatapack;
public Pack(Path path, boolean isDirectory, String id, LocalModFile.Description description, DataPack parentDataPack) {
public Pack(Path path, boolean isDirectory, String id, LocalModFile.Description description, Datapack parentDatapack) {
this.path = path;
this.isDirectory = isDirectory;
this.id = id;
this.description = description;
this.parentDataPack = parentDataPack;
this.parentDatapack = parentDatapack;
this.statusFile = initializeStatusFile(path, isDirectory);
this.activeProperty = initializeActiveProperty();
@@ -313,8 +313,8 @@ public class DataPack {
return description;
}
public DataPack getParentDataPack() {
return parentDataPack;
public Datapack getParentDatapack() {
return parentDatapack;
}
public BooleanProperty activeProperty() {
@@ -23,17 +23,12 @@ import org.jackhuang.hmcl.task.Task;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.List;
import java.util.Set;
/**
*
* @author huangyuhui
*/
public abstract class Modpack {
public static final Set<String> SUPPORTED_ICON_EXTS = Set.of("png", "jpg", "jpeg", "bmp", "gif", "webp", "apng");
public static final Set<String> SUPPORTED_ICON_NAMES = Set.of("icon.png", "icon.jpg", "icon.jpeg", "icon.bmp", "icon.gif", "icon.webp", "icon.apng");
private String name;
private String author;
private String version;
@@ -1,20 +1,3 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.mod;
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackManifest;
@@ -22,20 +22,17 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.GameBuilder;
import org.jackhuang.hmcl.game.DefaultGameRepository;
import org.jackhuang.hmcl.mod.*;
import org.jackhuang.hmcl.task.CacheFileTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* Install a downloaded CurseForge modpack.
@@ -50,11 +47,8 @@ public final class CurseInstallTask extends Task<Void> {
private final Modpack modpack;
private final CurseManifest manifest;
private final String name;
private final String iconUrl;
private final Path run;
private final ModpackConfiguration<CurseManifest> config;
private String iconExt;
private Task<Path> downloadIconTask;
private final List<Task<?>> dependents = new ArrayList<>(4);
private final List<Task<?>> dependencies = new ArrayList<>(1);
@@ -66,13 +60,12 @@ public final class CurseInstallTask extends Task<Void> {
* @param manifest The manifest content of given CurseForge modpack.
* @param name the new version name
*/
public CurseInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, Modpack modpack, CurseManifest manifest, String name, String iconUrl) {
public CurseInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, Modpack modpack, CurseManifest manifest, String name) {
this.dependencyManager = dependencyManager;
this.zipFile = zipFile;
this.modpack = modpack;
this.manifest = manifest;
this.name = name;
this.iconUrl = iconUrl;
this.repository = dependencyManager.getGameRepository();
this.run = repository.getRunDirectory(name);
@@ -115,14 +108,6 @@ public final class CurseInstallTask extends Task<Void> {
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList(manifest.overrides()), any -> true, config).withStage("hmcl.modpack"));
dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList(manifest.overrides()), manifest, CurseModpackProvider.INSTANCE, manifest.name(), manifest.version(), repository.getModpackConfiguration(name)).withStage("hmcl.modpack"));
URI iconUri = NetworkUtils.toURIOrNull(iconUrl);
if (iconUri != null) {
String ext = FileUtils.getExtension(StringUtils.substringAfter(iconUri.getPath(), '/')).toLowerCase(Locale.ROOT);
if (Modpack.SUPPORTED_ICON_EXTS.contains(ext)) {
iconExt = ext;
dependents.add(downloadIconTask = new CacheFileTask(dependencyManager.getDownloadProvider().injectURLWithCandidates(iconUrl)));
}
}
dependencies.add(new CurseCompletionTask(dependencyManager, name, manifest));
}
@@ -152,13 +137,5 @@ public final class CurseInstallTask extends Task<Void> {
Path root = repository.getVersionRoot(name);
Files.createDirectories(root);
JsonUtils.writeToJsonFile(root.resolve("manifest.json"), manifest);
if (iconExt != null && Modpack.SUPPORTED_ICON_NAMES.stream().map(root::resolve).allMatch(Files::notExists)) {
try {
Files.copy(downloadIconTask.getResult(), root.resolve("icon." + iconExt));
} catch (Exception e) {
LOG.warning("Failed to copy modpack icon", e);
}
}
}
}
@@ -52,7 +52,7 @@ public final class CurseModpackProvider implements ModpackProvider {
if (!(modpack.getManifest() instanceof CurseManifest curseManifest))
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new CurseInstallTask(dependencyManager, zipFile, modpack, curseManifest, name, null));
return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new CurseInstallTask(dependencyManager, zipFile, modpack, curseManifest, name));
}
@Override
@@ -69,7 +69,7 @@ public final class CurseModpackProvider implements ModpackProvider {
return new Modpack(manifest.name(), manifest.author(), manifest.version(), manifest.minecraft().gameVersion(), description, encoding, manifest) {
@Override
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name, String iconUrl) {
return new CurseInstallTask(dependencyManager, zipFile, this, manifest, name, iconUrl);
return new CurseInstallTask(dependencyManager, zipFile, this, manifest, name);
}
};
}
@@ -1,20 +1,3 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.mod.modinfo;
import com.google.gson.JsonArray;
@@ -1,20 +1,3 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.mod.modinfo;
import com.google.gson.JsonObject;
@@ -38,6 +38,7 @@ import java.util.*;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class ModrinthInstallTask extends Task<Void> {
private static final Set<String> SUPPORTED_ICON_EXTS = Set.of("png", "jpg", "jpeg", "bmp", "gif", "webp", "apng");
private final DefaultDependencyManager dependencyManager;
private final DefaultGameRepository repository;
@@ -120,7 +121,7 @@ public class ModrinthInstallTask extends Task<Void> {
URI iconUri = NetworkUtils.toURIOrNull(iconUrl);
if (iconUri != null) {
String ext = FileUtils.getExtension(StringUtils.substringAfter(iconUri.getPath(), '/')).toLowerCase(Locale.ROOT);
if (Modpack.SUPPORTED_ICON_EXTS.contains(ext)) {
if (SUPPORTED_ICON_EXTS.contains(ext)) {
iconExt = ext;
dependents.add(downloadIconTask = new CacheFileTask(dependencyManager.getDownloadProvider().injectURLWithCandidates(iconUrl)));
@@ -156,7 +157,7 @@ public class ModrinthInstallTask extends Task<Void> {
Files.createDirectories(root);
JsonUtils.writeToJsonFile(root.resolve("modrinth.index.json"), manifest);
if (iconExt != null && Modpack.SUPPORTED_ICON_NAMES.stream().map(root::resolve).allMatch(Files::notExists)) {
if (iconExt != null) {
try {
Files.copy(downloadIconTask.getResult(), root.resolve("icon." + iconExt));
} catch (Exception e) {
@@ -1,20 +1,3 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.mod.modrinth;
import java.io.File;
@@ -1,20 +1,3 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.mod.multimc;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
@@ -1,20 +1,3 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.resourcepack;
import org.jackhuang.hmcl.mod.LocalModFile;
@@ -1,20 +1,3 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.resourcepack;
import org.jackhuang.hmcl.mod.LocalModFile;
@@ -1,20 +1,3 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.resourcepack;
import org.jackhuang.hmcl.mod.LocalModFile;
@@ -17,9 +17,9 @@
*/
package org.jackhuang.hmcl.schematic;
import com.github.steveice10.opennbt.NBTIO;
import com.github.steveice10.opennbt.tag.builtin.*;
import javafx.geometry.Point3D;
import org.glavo.nbt.io.NBTCodec;
import org.glavo.nbt.tag.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -30,19 +30,32 @@ import java.nio.file.Path;
import java.time.Instant;
import java.util.zip.GZIPInputStream;
/// @author Glavo
/// @see <a href="https://litemapy.readthedocs.io/en/v0.9.0b0/litematics.html">The Litematic file format</a>
/**
* @author Glavo
* @see <a href="https://litemapy.readthedocs.io/en/v0.9.0b0/litematics.html">The Litematic file format</a>
*/
public final class LitematicFile {
private static int tryGetInt(Tag tag) {
return tag instanceof IntTag ? ((IntTag) tag).getValue() : 0;
}
private static @Nullable Instant tryGetLongTimestamp(Tag tag) {
if (tag instanceof LongTag) {
return Instant.ofEpochMilli(((LongTag) tag).getValue());
}
return null;
}
private static @Nullable String tryGetString(Tag tag) {
return tag instanceof StringTag stringTag ? stringTag.get() : null;
return tag instanceof StringTag ? ((StringTag) tag).getValue() : null;
}
public static LitematicFile load(Path file) throws IOException {
CompoundTag root;
try (InputStream in = new GZIPInputStream(Files.newInputStream(file))) {
root = NBTCodec.of().readTag(in, TagType.COMPOUND);
root = (CompoundTag) NBTIO.readTag(in);
}
Tag versionTag = root.get("Version");
@@ -58,13 +71,14 @@ public final class LitematicFile {
throw new IOException("Metadata tag is not a compound tag");
int regions = 0;
if (root.get("Regions") instanceof CompoundTag regionsTag)
regions = regionsTag.size();
Tag regionsTag = root.get("Regions");
if (regionsTag instanceof CompoundTag)
regions = ((CompoundTag) regionsTag).size();
return new LitematicFile(file, (CompoundTag) metadataTag,
((IntTag) versionTag).getValue(),
root.getIntOrZero("SubVersion"),
root.getIntOrZero("MinecraftDataVersion"),
tryGetInt(root.get("SubVersion")),
tryGetInt(root.get("MinecraftDataVersion")),
regions
);
}
@@ -94,23 +108,26 @@ public final class LitematicFile {
this.regionCount = regionCount;
Tag previewImageData = metadata.get("PreviewImageData");
this.previewImageData = previewImageData instanceof IntArrayTag intArrayTag
? intArrayTag.getArray()
this.previewImageData = previewImageData instanceof IntArrayTag
? ((IntArrayTag) previewImageData).getValue()
: null;
this.name = tryGetString(metadata.get("Name"));
this.author = tryGetString(metadata.get("Author"));
this.description = tryGetString(metadata.get("Description"));
this.timeCreated = metadata.get("TimeCreated") instanceof LongTag time ? Instant.ofEpochMilli(time.getValue()) : null;
this.timeModified = metadata.get("TimeModified") instanceof LongTag time ? Instant.ofEpochMilli(time.getValue()) : null;
this.totalBlocks = metadata.getIntOrZero("TotalBlocks");
this.totalVolume = metadata.getIntOrZero("TotalVolume");
this.timeCreated = tryGetLongTimestamp(metadata.get("TimeCreated"));
this.timeModified = tryGetLongTimestamp(metadata.get("TimeModified"));
this.totalBlocks = tryGetInt(metadata.get("TotalBlocks"));
this.totalVolume = tryGetInt(metadata.get("TotalVolume"));
Point3D enclosingSize = null;
if (metadata.get("EnclosingSize") instanceof CompoundTag list) {
int x = list.getIntOrZero("x");
int y = list.getIntOrZero("y");
int z = list.getIntOrZero("z");
Tag enclosingSizeTag = metadata.get("EnclosingSize");
if (enclosingSizeTag instanceof CompoundTag) {
CompoundTag list = (CompoundTag) enclosingSizeTag;
int x = tryGetInt(list.get("x"));
int y = tryGetInt(list.get("y"));
int z = tryGetInt(list.get("z"));
if (x >= 0 && y >= 0 && z >= 0)
enclosingSize = new Point3D(x, y, z);
@@ -1,20 +1,3 @@
/*
* 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.task;
import java.util.Collection;
@@ -1,20 +1,3 @@
/*
* 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.Contract;
@@ -1,20 +1,3 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util;
import javafx.beans.InvalidationListener;
@@ -15,6 +15,7 @@
* 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;
@@ -79,6 +79,20 @@ public final class Lang {
return Collections.unmodifiableList(Arrays.asList(elements));
}
public static <T extends Comparable<T>> T clamp(T min, T val, T max) {
if (val.compareTo(min) < 0) return min;
else if (val.compareTo(max) > 0) return max;
else return val;
}
public static double clamp(double min, double val, double max) {
return Math.max(min, Math.min(val, max));
}
public static int clamp(int min, int val, int max) {
return Math.max(min, Math.min(val, max));
}
public static boolean test(ExceptionalRunnable<?> r) {
try {
r.run();
@@ -1,39 +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;
/// Utility class for mathematical operations.
///
/// @author Glavo
public final class MathUtils {
/// Clamp value to fit between min and max.
public static double clamp(double value, double min, double max) {
assert min <= max;
return Math.min(max, Math.max(value, min));
}
/// Clamp value to fit between min and max.
public static int clamp(int value, int min, int max) {
assert min <= max;
return Math.min(max, Math.max(value, min));
}
private MathUtils() {
}
}
@@ -86,9 +86,4 @@ public final class SettingsMap {
public void clear() {
map.clear();
}
@Override
public String toString() {
return "SettingsMap" + map;
}
}
@@ -1,20 +1,3 @@
/*
* 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.gson;
public @interface JsonSubtype {
@@ -1,20 +1,3 @@
/*
* 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.gson;
import java.lang.annotation.ElementType;
@@ -1,20 +1,3 @@
/*
* 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.gson;
import com.google.gson.*;
@@ -15,6 +15,7 @@
* 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.io;
import org.jackhuang.hmcl.util.InfiniteSizeList;
@@ -1,20 +1,3 @@
/*
* 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.io;
import java.io.IOException;
@@ -282,25 +282,6 @@ public enum OperatingSystem {
return SYSTEM_VERSION.isAtLeast(OSVersion.WINDOWS_7);
}
public static boolean isRunningUnderWine() {
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS) {
return false;
}
String[] wineEnvVars = {
"WINEPREFIX",
"WINELOADER",
"WINEDLLPATH"
};
for (String var : wineEnvVars) {
if (System.getenv(var) != null) {
return true;
}
}
return false;
}
public static Path getWorkingDirectory(String folder) {
String home = System.getProperty("user.home", ".");
switch (OperatingSystem.CURRENT_OS) {
@@ -15,6 +15,7 @@
* 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.hardware;
import org.jetbrains.annotations.Contract;
@@ -15,6 +15,7 @@
* 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.tree;
import kala.compress.archivers.tar.TarArchiveEntry;
@@ -19,7 +19,6 @@ package org.jackhuang.hmcl.util.versioning;
import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.IOException;
@@ -190,32 +189,6 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
return buildDebugString().toString();
}
public static @Nullable GameVersionNumber getReleaseOfSnapshot(GameVersionNumber gameVersion) {
if (gameVersion instanceof Release release) {
if (release.getEaType() == Release.ReleaseType.GA) {
return null;
}
if (release.getPatch() > 0) {
return asGameVersion(release.getMajor() + "." + release.getMinor() + "." + release.getPatch());
} else {
return asGameVersion(release.getMajor() + "." + release.getMinor());
}
}
if (gameVersion instanceof LegacySnapshot snapshot) {
String[] defaultVersions = Versions.DEFAULT_GAME_VERSIONS;
for (int i = defaultVersions.length - 1; i >= 0; i--) {
Release gaRelease = (Release) asGameVersion(defaultVersions[i]);
if (gaRelease.compareToSnapshot(snapshot) > 0) {
return gaRelease;
}
}
}
return null;
}
public static final class Old extends GameVersionNumber {
static Old parse(String value) {
if (value.isEmpty())
@@ -1,20 +1,3 @@
/*
* 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.versioning;
import java.util.Objects;
@@ -1,4 +1,4 @@
# https://gitlab.freedesktop.org/mesa/libdrm/-/blob/15cdcfe9e1c4b00f2a819bc1f4b3efa1326ac6bc/data/amdgpu.ids
# https://gitlab.freedesktop.org/mesa/libdrm/-/blob/35a21916c8f621b35a67f3c5994280ae59a4981a/data/amdgpu.ids
#
# List of AMDGPU IDs
#
@@ -10,19 +10,6 @@
1114, C3, AMD Radeon 840M Graphics
1114, D2, AMD Radeon 860M Graphics
1114, D3, AMD Radeon 840M Graphics
1114, E2, AMD Radeon 860M Graphics
1114, E4, AMD Radeon 860M Graphics
1114, E5, AMD Radeon 840M Graphics
1114, E9, AMD Radeon 860M Graphics
1114, EA, AMD Radeon 840M Graphics
1114, ED, AMD Radeon 860M Graphics
1114, EE, AMD Radeon 840M Graphics
1114, F2, AMD Radeon 860M Graphics
1114, F3, AMD Radeon 840M Graphics
1114, F9, AMD Radeon 860M Graphics
1114, FA, AMD Radeon 840M Graphics
1114, FC, AMD Radeon 860M Graphics
1114, FD, AMD Radeon 840M Graphics
1309, 00, AMD Radeon R7 Graphics
130A, 00, AMD Radeon R6 Graphics
130B, 00, AMD Radeon R4 Graphics
@@ -54,35 +41,23 @@
1506, C3, AMD Radeon 610M
1506, C4, AMD Radeon 610M
150E, C1, AMD Radeon 890M Graphics
150E, C4, AMD Radeon 880M Graphics
150E, C4, AMD Radeon 890M Graphics
150E, C5, AMD Radeon 890M Graphics
150E, C6, AMD Radeon 890M Graphics
150E, C7, AMD Radeon 890M Graphics
150E, D1, AMD Radeon 890M Graphics
150E, D2, AMD Radeon 880M Graphics
150E, D2, AMD Radeon 890M Graphics
150E, D3, AMD Radeon 890M Graphics
150E, E1, AMD Radeon 890M Graphics
150E, E3, AMD Radeon 890M Graphics
150E, E4, AMD Radeon 890M Graphics
150E, F1, AMD Radeon 890M Graphics
150E, F3, AMD Radeon 890M Graphics
1586, C1, AMD Radeon 8060S Graphics
1586, C2, AMD Radeon 8050S Graphics
1586, C3, AMD Radeon 8060S Graphics
1586, C4, AMD Radeon 8050S Graphics
1586, C6, AMD Radeon 8060S Graphics
1586, D1, AMD Radeon 8060S Graphics
1586, D2, AMD Radeon 8050S Graphics
1586, D4, AMD Radeon 8050S Graphics
1586, D5, AMD Radeon 8040S Graphics
1586, C1, Radeon 8060S Graphics
1586, C2, Radeon 8050S Graphics
1586, C4, Radeon 8050S Graphics
1586, D1, Radeon 8060S Graphics
1586, D2, Radeon 8050S Graphics
1586, D4, Radeon 8050S Graphics
1586, D5, Radeon 8040S Graphics
15BF, 00, AMD Radeon 780M Graphics
15BF, 01, AMD Radeon 760M Graphics
15BF, 02, AMD Radeon 780M Graphics
15BF, 03, AMD Radeon 760M Graphics
15BF, 05, AMD Radeon 760M Graphics
15BF, 06, AMD Radeon 780M Graphics
15BF, 07, AMD Radeon 740M Graphics
15BF, 08, AMD Radeon 740M Graphics
15BF, C1, AMD Radeon 780M Graphics
15BF, C2, AMD Radeon 780M Graphics
15BF, C3, AMD Radeon 760M Graphics
@@ -96,7 +71,6 @@
15BF, CB, AMD Radeon 760M Graphics
15BF, CC, AMD Radeon 740M Graphics
15BF, CD, AMD Radeon 760M Graphics
15BF, CE, AMD Radeon 740M Graphics
15BF, CF, AMD Radeon 780M Graphics
15BF, D0, AMD Radeon 780M Graphics
15BF, D1, AMD Radeon 780M Graphics
@@ -119,18 +93,10 @@
15C8, C2, AMD Radeon 740M Graphics
15C8, C3, AMD Radeon 740M Graphics
15C8, C4, AMD Radeon 740M Graphics
15C8, C5, AMD Radeon 740M Graphics
15C8, C6, AMD Radeon 740M Graphics
15C8, C7, AMD Radeon 740M Graphics
15C8, C8, AMD Radeon 740M Graphics
15C8, D1, AMD Radeon 740M Graphics
15C8, D2, AMD Radeon 740M Graphics
15C8, D3, AMD Radeon 740M Graphics
15C8, D4, AMD Radeon 740M Graphics
15C8, D5, AMD Radeon 740M Graphics
15C8, D6, AMD Radeon 740M Graphics
15C8, D7, AMD Radeon 740M Graphics
15C8, D8, AMD Radeon 740M Graphics
15D8, 00, AMD Radeon RX Vega 8 Graphics WS
15D8, 91, AMD Radeon Vega 3 Graphics
15D8, 91, AMD Ryzen Embedded R1606G with Radeon Vega Gfx
@@ -286,19 +252,6 @@
1901, D6, AMD Radeon 740M Graphics
1901, D7, AMD Radeon 740M Graphics
1901, D8, AMD Radeon 740M Graphics
1902, C0, AMD Radeon 840M Graphics
1902, C1, AMD Radeon 840M Graphics
1902, C2, AMD Radeon 820M Graphics
1902, C3, AMD Radeon 840M Graphics
1902, C6, AMD Radeon 820M Graphics
1902, C7, AMD Radeon 840M Graphics
1902, C8, AMD Radeon 840M Graphics
1902, C9, AMD Radeon 820M Graphics
1902, CA, AMD Radeon 840M Graphics
1902, D1, AMD Radeon 840M Graphics
1902, D3, AMD Radeon 840M Graphics
1902, D7, AMD Radeon 840M Graphics
1902, D8, AMD Radeon 840M Graphics
6600, 00, AMD Radeon HD 8600 / 8700M
6600, 81, AMD Radeon R7 M370
6601, 00, AMD Radeon HD 8500M / 8700M
@@ -626,7 +579,6 @@
7480, C3, AMD Radeon RX 7600S
7480, C7, AMD Radeon RX 7600M XT
7480, CF, AMD Radeon RX 7600
7481, C7, AMD Steam Machine
7483, CF, AMD Radeon RX 7600M
7489, 00, AMD Radeon Pro W7500
7499, 00, AMD Radeon Pro W7400
@@ -645,14 +597,12 @@
7550, C2, AMD Radeon RX 9070 GRE
7550, C3, AMD Radeon RX 9070
7551, C0, AMD Radeon AI PRO R9700
7551, C8, AMD Radeon AI PRO R9600D
7590, C0, AMD Radeon RX 9060 XT
7590, C1, AMD Radeon RX 9060 XT LP
7590, C7, AMD Radeon RX 9060
75A0, 00, AMD Instinct MI350X
75A3, 00, AMD Instinct MI355X
75B0, 00, AMD Instinct MI350X VF
75B3, 00, AMD Instinct MI355X VF
75A0, C0, AMD Instinct MI350X
75A3, C0, AMD Instinct MI355X
75B0, C0, AMD Instinct MI350X VF
75B3, C0, AMD Instinct MI355X VF
9830, 00, AMD Radeon HD 8400 / R3 Series
9831, 00, AMD Radeon HD 8400E
9832, 00, AMD Radeon HD 8330
@@ -1,20 +1,3 @@
/*
* 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.junit.jupiter.api.Test;
Loaded 100 of 115 files, more files were not shown because too many files have changed in this diff. Show more