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
98 changed files with 1049 additions and 1624 deletions

No files matched your search

+2
View File
@@ -66,6 +66,7 @@ dependencies {
}
embedResources(libs.authlib.injector)
embedResources(libs.lwjgl.unsafe.agent)
}
fun digest(algorithm: String, bytes: ByteArray): ByteArray = MessageDigest.getInstance(algorithm).digest(bytes)
@@ -155,6 +156,7 @@ val hmclProperties = buildList {
add("hmcl.microsoft.auth.id" to microsoftAuthId)
add("hmcl.curseforge.apikey" to curseForgeApiKey)
add("hmcl.authlib-injector.version" to libs.authlib.injector.get().version!!)
add("hmcl.lwjgl-unsafe-agent.version" to libs.lwjgl.unsafe.agent.get().version!!)
}
val hmclPropertiesFile = layout.buildDirectory.file("hmcl.properties")
@@ -49,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;
@@ -154,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
@@ -179,7 +179,7 @@ public class JFXCustomColorPickerDialog extends StackPane {
(int) (newColor.getRed() * 255),
(int) (newColor.getGreen() * 255),
(int) (newColor.getBlue() * 255));
String rgb = String.format("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));
@@ -294,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
}
@@ -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;
@@ -21,12 +21,16 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.launch.DefaultLauncher;
import org.jackhuang.hmcl.launch.ProcessListener;
import org.jackhuang.hmcl.util.NativePatcher;
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.*;
import java.util.*;
import java.util.stream.Stream;
@@ -150,4 +154,60 @@ public final class HMCLGameLauncher extends DefaultLauncher {
generateOptionsTxt();
super.makeLaunchScript(scriptFile);
}
@Override
protected void appendJvmArgs(CommandBuilder result) {
super.appendJvmArgs(result);
if (config().getAllowAutoAgent()
&& !options.isNoGeneratedJVMArgs()
&& !options.isNoGeneratedOptimizingJVMArgs()
&& NativePatcher.needPatchMemoryUtil(version, options.getJava().getParsedVersion())) {
LOG.info("Attempting to patch game with lwjgl-unsafe-agent");
try {
result.add("-javaagent:" + extractLwjglUnsafeAgent());
} catch (Exception e) {
LOG.warning("Failed to extract lwjgl-unsafe-agent", e);
}
}
}
private Path extractLwjglUnsafeAgent() throws IOException {
String agentVersion = JarUtils.getAttribute("hmcl.lwjgl-unsafe-agent.version", null);
if (agentVersion == null) {
throw new IOException("Missing hmcl.lwjgl-unsafe-agent.version attribute");
}
Library library = new Library(new Artifact("org.glavo", "lwjgl-unsafe-agent", agentVersion));
String fileName = library.getArtifact().getFileName();
Path agentPath = repository.getLibraryFile(version, library).toAbsolutePath().normalize();
if (agentPath.toString().contains("=")) {
throw new IOException("Invalid library path: " + agentPath);
}
byte[] bytes;
try (InputStream input = DefaultLauncher.class.getResourceAsStream("/assets/" + fileName)) {
if (input == null) {
throw new IOException("/assets/" + fileName + " not found");
}
bytes = input.readAllBytes();
}
if (Files.isRegularFile(agentPath)) {
try {
if (Files.size(agentPath) == bytes.length) {
return agentPath;
}
} catch (IOException e) {
LOG.warning("Failed to check size of " + agentPath, e);
}
}
Files.createDirectories(agentPath.getParent());
FileUtils.saveSafely(agentPath, output -> output.write(bytes));
return agentPath;
}
}
@@ -65,6 +65,7 @@ import java.util.stream.Collectors;
import static javafx.application.Platform.runLater;
import static javafx.application.Platform.setImplicitExit;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
import static org.jackhuang.hmcl.util.Lang.resolveException;
@@ -75,6 +76,8 @@ import static org.jackhuang.hmcl.util.platform.Platform.isCompatibleWithX86Java;
public final class LauncherHelper {
private static final String LWJGL_3_4_1_TIP = "lwjgl3.4.1-ffm";
private final Profile profile;
private Account account;
private final String selectedVersion;
@@ -197,6 +200,28 @@ public final class LauncherHelper {
);
}).withStage("launch.state.dependencies")
.thenComposeAsync(() -> gameVersion.map(s -> new GameVerificationFixTask(dependencyManager, s, version.get())).orElse(null))
.thenComposeAsync(() -> {
if (config().getAllowAutoAgent()
|| setting.isNoJVMArgs()
|| setting.isNoOptimizingJVMArgs()
|| Boolean.TRUE.equals(config().getShownTips().get(LWJGL_3_4_1_TIP))
|| !NativePatcher.needPatchMemoryUtil(version.get(), javaVersionRef.get().getParsedVersion())) {
return Task.completed(null);
} else {
CompletableFuture<Void> future = new CompletableFuture<>();
runInFX(() -> {
Controllers.confirm(i18n("launch.advice.lwjgl_3_4_1"), i18n("launch.advice.lwjgl_3_4_1.title"), MessageType.QUESTION, () -> {
config().getShownTips().put(LWJGL_3_4_1_TIP, true);
config().setAllowAutoAgent(true);
future.complete(null);
}, () -> {
config().getShownTips().put(LWJGL_3_4_1_TIP, true);
future.complete(null);
});
});
return Task.fromCompletableFuture(future);
}
})
.thenComposeAsync(() -> logIn(account).withStage("launch.state.logging_in"))
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
LaunchOptions.Builder launchOptionsBuilder = repository.getLaunchOptions(
@@ -689,6 +689,21 @@ public final class Config extends ObservableSetting {
this.disableAutoGameOptions.set(disableAutoGameOptions);
}
@SerializedName("allowAutoAgent")
private final BooleanProperty allowAutoAgent = new SimpleBooleanProperty(false);
public BooleanProperty allowAutoAgentProperty() {
return allowAutoAgent;
}
public boolean getAllowAutoAgent() {
return allowAutoAgent.get();
}
public void setAllowAutoAgent(boolean allowAutoAgent) {
this.allowAutoAgent.set(allowAutoAgent);
}
// Accounts
@SerializedName("authlibInjectorServers")
@@ -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 {
@@ -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()));
}
}
@@ -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,6 +19,7 @@ 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;
@@ -35,7 +36,6 @@ 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,12 +61,12 @@ import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -289,6 +288,15 @@ public final class SettingsPage extends ScrollPane {
settingsPane.getContent().add(disableAutoGameOptionsPane);
}
{
LineToggleButton allowAutoAgentPane = new LineToggleButton();
allowAutoAgentPane.setTitle(i18n("settings.launcher.allow_auto_agent"));
allowAutoAgentPane.setSubtitle(i18n("settings.launcher.allow_auto_agent.subtitle"));
allowAutoAgentPane.selectedProperty().bindBidirectional(config().allowAutoAgentProperty());
settingsPane.getContent().add(allowAutoAgentPane);
}
{
BorderPane debugPane = new BorderPane();
@@ -302,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);
@@ -398,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,
@@ -61,12 +61,7 @@ public final class HMCLLocalizedDownloadListPage extends DownloadListPage {
repository = new Repository(type, curseForge, modrinth);
supportChinese.set(true);
downloadSources.setAll("mods.modrinth");
if (CurseForgeRemoteModRepository.isAvailable()) {
downloadSources.add("mods.curseforge");
}
downloadSources.get().setAll("mods.modrinth", "mods.curseforge");
if (modrinth != null) {
downloadSource.set("mods.modrinth");
} else if (curseForge != 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.versions;
import com.jfoenix.controls.JFXButton;
@@ -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);
}
}
@@ -31,21 +31,27 @@ public final class ChunkBaseApp {
private static final GameVersionNumber MIN_END_CITY_VERSION = GameVersionNumber.asGameVersion("1.13");
private static final String[] SEED_MAP_GAME_VERSIONS = {
"1.21.9", "1.21.6", "1.21.5", "1.21.4", "1.21.2", "1.21", "1.20",
"1.19.3", "1.19", "1.18", "1.17", "1.16", "1.15", "1.14", "1.13",
"1.12", "1.11", "1.10", "1.9", "1.8", "1.7"
"26.1", "1.21.9", "1.21.6", "1.21.5", "1.21.4", "1.21.2", "1.21",
"1.20", "1.19.3", "1.19", "1.18", "1.17", "1.16", "1.15", "1.14",
"1.13", "1.12", "1.11", "1.10", "1.9", "1.8", "1.7"
};
public static final String[] STRONGHOLD_FINDER_GAME_VERSIONS = {
"1.20", "1.19.3", "1.19", "1.18", "1.16", "1.13", "1.9", "1.7"
"26.1", "1.21.9", "1.21.6", "1.21.5", "1.21.4", "1.21.2", "1.21",
"1.20", "1.19.3", "1.19", "1.18", "1.17", "1.16", "1.15", "1.14",
"1.13", "1.12", "1.11", "1.10", "1.9", "1.8", "1.7"
};
public static final String[] NETHER_FORTRESS_GAME_VERSIONS = {
"1.18", "1.16", "1.7"
"26.1", "1.21.9", "1.21.6", "1.21.5", "1.21.4", "1.21.2", "1.21",
"1.20", "1.19.3", "1.19", "1.18", "1.17", "1.16", "1.15", "1.14",
"1.13", "1.12", "1.11", "1.10", "1.9", "1.8", "1.7"
};
public static final String[] END_CITY_GAME_VERSIONS = {
"1.19", "1.13"
"26.1", "1.21.9", "1.21.6", "1.21.5", "1.21.4", "1.21.2", "1.21",
"1.20", "1.19.3", "1.19", "1.18", "1.17", "1.16", "1.15", "1.14",
"1.13"
};
public static boolean isSupported(@NotNull World world) {
@@ -63,6 +63,16 @@ public final class NativePatcher {
});
}
// https://github.com/LWJGL/lwjgl3/issues/1111
public static boolean needPatchMemoryUtil(Version version, int javaVersion) {
return javaVersion >= 25 && javaVersion <= 26 && version.getLibraries().stream().anyMatch(library ->
"org.lwjgl".equals(library.getGroupId())
&& "lwjgl".equals(library.getArtifactId())
&& "3.4.1".equals(library.getVersion())
&& library.getClassifier() == null
);
}
public static Version patchNative(DefaultGameRepository repository,
Version version, String gameVersion,
JavaRuntime javaVersion,
@@ -155,7 +155,32 @@ public class Translator_lzh extends Translator {
case "23w13a_or_b" -> "二三週一三暨";
case "24w14potato" -> "二四週一四芋";
case "25w14craftmine" -> "二五週一四礦";
default -> version;
default -> {
if (version.length() >= 6 && version.charAt(2) == 'w') {
Matcher matcher = Pattern.compile("(?<year>[0-9]{2})w(?<week>[0-9]{2})(?<suffix>.+)").matcher(version);
if (matcher.matches()) {
int year = Integer.parseInt(matcher.group("year"));
int week = Integer.parseInt(matcher.group("week"));
String suffix = matcher.group("suffix");
char suffixFirstChar = suffix.charAt(0);
StringBuilder builder = new StringBuilder();
appendDigitByDigit(builder, String.valueOf(year));
builder.append('週');
appendDigitByDigit(builder, String.valueOf(week));
if (suffix.length() == 1
&& suffixFirstChar >= 'a' && (suffixFirstChar - 'a') < TIAN_GAN.length)
builder.append(TIAN_GAN[suffixFirstChar - 'a']);
else
builder.append(suffix);
yield builder.toString();
}
}
yield version;
}
};
} else {
return gameVersion.toString();
@@ -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",
+15 -15
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;
}
@@ -1923,4 +1923,4 @@
.line-toggle-button .jfx-toggle-button {
-fx-padding: 0;
}
}
@@ -808,6 +808,8 @@ launch.advice.forge37_0_60=Forge versions prior to 37.0.60 are not compatible wi
launch.advice.java8_1_13=Minecraft 1.13 and later can only be run on Java 8 or later. Please use Java 8 or later versions.
launch.advice.java8_51_1_13=Minecraft 1.13 may crash on Java 8 versions prior to 1.8.0_51. Please install the latest Java 8 version.
launch.advice.java9=You cannot launch Minecraft 1.12 or earlier with Java 9 or later. Please use Java 8 instead.
launch.advice.lwjgl_3_4_1=The current game version has a known issue that may cause stuttering in the game.\nHMCL can automatically optimize the game performance (using a Java Agent).\nDo you allow HMCL to automatically optimize the game?\nIf you encounter issues, you can disable the "Allow HMCL to modify the game" option in "Settings → General".
launch.advice.lwjgl_3_4_1.title=Allow HMCL to automatically optimize the game?
launch.advice.modded_java=Some mods may not be compatible with newer Java versions. It is recommended to use Java %1$s to launch Minecraft %2$s.
launch.advice.modlauncher8=The Forge version you are using is not compatible with the current Java version. Please try updating Forge.
launch.advice.newer_java=You are using an older Java version to launch the game. It is recommended to update to Java 8, otherwise some mods may cause the game to crash.
@@ -1381,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
@@ -1417,6 +1418,8 @@ settings.game.working_directory.hint=Enable the "Isolated" option in "Working Di
settings.icon=Icon
settings.launcher=Launcher Settings
settings.launcher.allow_auto_agent=Allow HMCL to modify the game
settings.launcher.allow_auto_agent.subtitle=Allow HMCL to modify the game by attaching a Java Agent to improve the gaming experience.
settings.launcher.appearance=Appearance
settings.launcher.brightness=Theme Mode
settings.launcher.brightness.auto=Follow System Settings
@@ -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=أمر التغليف
@@ -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
@@ -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=パッキングオーダー
@@ -579,6 +579,8 @@ launch.advice.forge37_0_60=鍛三七點〇點五九與舊者弗兼爪哇十七
launch.advice.java8_1_13=礦藝一點一三以晉但行於爪哇八與晉者。宜行以爪哇八與晉者。
launch.advice.java8_51_1_13=爪哇之前一點八點〇之五一者或崩礦藝一點一三。宜新爪哇於一點八點〇之五一以晉,然後復啟。
launch.advice.java9=礦藝之前一點一三而有改囊者弗見兼於爪哇九與晉者。宜啟以爪哇八。
launch.advice.lwjgl_3_4_1=戲之是版有已知之謬,行戲之時或致頓滯。\nHMCL 可假爪哇 Agent 自優其戲,以弭是患。君其許 HMCL 自優其戲乎?\n誠有謬,可至於「置設 → 貫用」,廢「許改戲」之項。
launch.advice.lwjgl_3_4_1.title=君其許 HMCL 自優其戲乎?
launch.advice.modded_java=改囊或弗見兼於爪哇之新者。宜以爪哇 %s 啟礦藝 %s。
launch.advice.modlauncher8=鍛版之所行弗兼於爪哇之所行。宜新鍛。
launch.advice.newer_java=余識君之啟戲以舊爪哇也,蓋改囊或是以崩戲也。宜新爪哇於八,然後復啟。
@@ -975,7 +977,10 @@ repositories.chooser=闕 JavaFX。HMCL 須之以行。\n擊「善」而引 JavaF
repositories.chooser.title=擇 JavaFX 之源
resourcepack=資囊
resourcepack.add=增資囊
resourcepack.download=引資囊
resourcepack.download.title=引資囊 - %1s
resourcepack.manage=司資囊
reveal.in_file_manager=覽於理案臺
@@ -1090,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=包裝之令
@@ -1124,6 +1128,8 @@ settings.game.working_directory.hint=夫于「運徑」擇「例獨」者,可
settings.icon=戲徵
settings.launcher=啟者置設
settings.launcher.allow_auto_agent=許改戲
settings.launcher.allow_auto_agent.subtitle=許 HMCL 假附加爪哇 Agent 以改戲,而優其戲感。
settings.launcher.appearance=外觀
settings.launcher.common_path.tooltip=啟者置戲資與府庫於是。倘戲有成案,則弗取公庫之案。
settings.launcher.debug=勘誤
@@ -1194,6 +1200,8 @@ sponsor.hmcl=Hello Minecraft! Launcher,乃一免費,自由,開源之礦藝
system.architecture=大構
system.operating_system=械網
terracotta=眾戲
unofficial.hint=子方用非官築之 HMCL。余無能證其安,請君自辨。
update=啟者迭更
@@ -1211,6 +1219,8 @@ update.channel.nightly.hint=君所用之 HMCL 乃預版也。預版之更迭常
update.channel.nightly.title=預版告
update.channel.stable=定版
update.checking=甫察其新
update.disable_auto_show_update_dialog=弗自顯迭更窗
update.disable_auto_show_update_dialog.subtitle=啟此,HMCL 將弗自顯迭更窗。
update.failed=迭更未成
update.found=尋得新者
update.newest_version=至新:%s
@@ -1220,6 +1230,8 @@ update.note=开發版與預版,虽增功、修阙,然亦恐生他患。
update.latest=當版至新
update.no_browser=無啟瀏覽器。址固鈔訖,君可自粘之以啟頁。
update.tooltip=迭更
update.preview=先覽 HMCL 之版
update.preview.subtitle=啟此,君可先得 HMCL 之新,試之於未行。
version=戲
version.name=戲例之名
@@ -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=Команда-оболочка
@@ -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=Команда обгортки
@@ -612,6 +612,8 @@ launch.advice.forge37_0_60=Forge 37.0.59 及更低版本與 Java 17 不相容。
launch.advice.java8_1_13=Minecraft 1.13 及更高版本僅支援 Java 8 或更高版本。請使用 Java 8 或最新版本。
launch.advice.java8_51_1_13=低於 1.8.0_51 的 Java 版本可能會導致 Minecraft 1.13 崩潰。建議你到 https://java.com 安裝最新版的 Java 8。
launch.advice.java9=低於 (包含) 1.13 的有安裝模組的 Minecraft 版本不支援 Java 9 或更高版本。請使用 Java 8。
launch.advice.lwjgl_3_4_1=當前遊戲版本存在已知問題,可能在遊戲中發生卡頓。\nHMCL 可以 (透過 Java Agent) 自動最佳化遊戲解決此問題。是否允許 HMCL 自動最佳化遊戲?\n如遇到問題,你可以在「設定 → 一般」中關閉「允許 HMCL 修改遊戲」選項。
launch.advice.lwjgl_3_4_1.title=是否允許 HMCL 自動最佳化遊戲?
launch.advice.modded_java=部分模組可能與高版本 Java 不相容。建議使用 Java %s 啟動 Minecraft %s。
launch.advice.modlauncher8=你所使用的 Forge 版本與目前使用的 Java 不相容。請更新 Forge。
launch.advice.newer_java=偵測到你正在使用舊版本 Java 啟動遊戲,這可能導致部分模組引發遊戲崩潰。建議更新至 Java 8 後再次啟動。
@@ -1168,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=前置指令
@@ -1202,6 +1203,8 @@ settings.game.working_directory.hint=在「執行路徑」選項中選取「各
settings.icon=遊戲圖示
settings.launcher=啟動器設定
settings.launcher.allow_auto_agent=允許 HMCL 修改遊戲
settings.launcher.allow_auto_agent.subtitle=允許 HMCL 透過附加 Java Agent 修改遊戲以改善遊戲體驗。
settings.launcher.appearance=外觀
settings.launcher.brightness=主題模式
settings.launcher.brightness.auto=跟隨系統設定
@@ -617,6 +617,8 @@ launch.advice.forge37_0_60=Forge 37.0.59 及更低版本与 Java 17 不兼容。
launch.advice.java8_1_13=Minecraft 1.13 及更高版本只能在 Java 8 或更高版本上运行。请使用 Java 8 或最新版本。
launch.advice.java8_51_1_13=低于 1.8.0_51 的 Java 版本可能会导致 Minecraft 1.13 崩溃。建议更新 Java 至 1.8.0_51 或更高版本后再次启动。
launch.advice.java9=低于 1.13 的有安装模组的 Minecraft 版本与 Java 9 或更高版本不兼容。请使用 Java 8。
launch.advice.lwjgl_3_4_1=当前游戏版本存在已知问题,可能在游戏中发生卡顿。\nHMCL 可以 (通过 Java Agent) 自动优化游戏解决此问题。是否允许 HMCL 自动优化游戏?\n如遇到问题,你可以在“设置 → 通用”中关闭“允许 HMCL 修改游戏”选项。
launch.advice.lwjgl_3_4_1.title=是否允许 HMCL 自动优化游戏?
launch.advice.modded_java=部分模组可能与高版本 Java 不兼容。建议使用 Java %s 启动 Minecraft %s。
launch.advice.modlauncher8=你所使用的 Forge 版本与当前使用的 Java 不兼容。请更新 Forge。
launch.advice.newer_java=检测到你正在使用旧版本 Java 启动游戏,这可能导致部分模组引发游戏崩溃。建议更新至 Java 8 后再次启动。
@@ -1173,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=包装命令
@@ -1207,6 +1208,8 @@ settings.game.working_directory.hint=在“版本隔离”中选择“各实例
settings.icon=游戏图标
settings.launcher=启动器设置
settings.launcher.allow_auto_agent=允许 HMCL 修改游戏
settings.launcher.allow_auto_agent.subtitle=允许 HMCL 通过附加 Java Agent 修改游戏以改善游戏体验。
settings.launcher.appearance=外观
settings.launcher.brightness=主题模式
settings.launcher.brightness.auto=跟随系统设置
@@ -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;
@@ -37,7 +37,7 @@ public record GameJavaVersion(String component, int majorVersion) {
public static final GameJavaVersion JAVA_16 = new GameJavaVersion("java-runtime-alpha", 16);
public static final GameJavaVersion JAVA_8 = new GameJavaVersion("jre-legacy", 8);
public static final GameJavaVersion LATEST = JAVA_21;
public static final GameJavaVersion LATEST = JAVA_25;
public static GameJavaVersion getMinimumJavaVersion(GameVersionNumber gameVersion) {
if (gameVersion.compareTo("26.1") >= 0)
@@ -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());
}
}
}
@@ -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 {
@@ -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() {
}
}
@@ -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;
@@ -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;
@@ -627,6 +627,10 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
throw new IllegalArgumentException(value);
}
if (year >= Release.MINIMUM_YEAR_MAJOR_VERSION) {
throw new IllegalArgumentException(value);
}
char suffix = value.charAt(5);
if (suffix < 'a' || suffix > 'z')
throw new IllegalArgumentException(value);
@@ -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;
@@ -912,3 +912,6 @@
26.1-rc-1
26.1-rc-2
26.1
26.1.1-rc-1
26.1.1
26w14a
@@ -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;
@@ -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;
@@ -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;
@@ -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 org.junit.jupiter.api.Test;
@@ -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 org.junit.jupiter.params.ParameterizedTest;
@@ -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.platform;
import org.junit.jupiter.api.Test;
@@ -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.skin;
import javafx.scene.image.Image;
@@ -376,10 +376,11 @@ public final class GameVersionNumberTest {
"1.21.11-pre1_unobfuscated",
"1.21.11-pre2",
"1.21.11-pre2_unobfuscated",
"99w99a",
"26.1-snapshot-1",
"26.1-snapshot-2",
"26.1",
"26.1.1",
"26w14a",
"26.2-snapshot-1",
"26.2-snapshot-2",
"26.2",
@@ -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 org.junit.jupiter.api.Test;
-2
View File
@@ -41,8 +41,6 @@ subprojects {
@Suppress("UnstableApiUsage")
tasks.withType<Checkstyle> {
maxHeapSize.set("2g")
setConfigProperties("licenseHeaderFile" to rootProject.rootDir.resolve("config/checkstyle/license-header.txt"))
}
configure<CheckstyleExtension> {
@@ -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.gradle;
import com.google.gson.Gson;
+3 -26
View File
@@ -1,7 +1,6 @@
<?xml version="1.0" ?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<?xml version="1.0" ?><!DOCTYPE module PUBLIC
"-//Puppy Crawl//DTD Check Configuration 1.2//EN"
"http://www.puppycrawl.com/dtds/configuration_1_2.dtd">
<module name="Checker">
<module name="NewlineAtEndOfFile"/> <!-- force newline, important for git merge and POSIX compatibility: http://checkstyle.sourceforge.net/config_misc.html#NewlineAtEndOfFile -->
@@ -118,26 +117,4 @@
<property name="message" value="Should not use String#toUpperCase(), use String#toUpperCase(Locale.ROOT) instead."/>
</module>
</module>
<module name="SuppressionSingleFilter">
<property name="checks" value="RegexpHeader"/>
<property name="files" value="^(?!.*[\\/]org[\\/]jackhuang[\\/]hmcl[\\/]).*$"/>
</module>
<module name="SuppressionSingleFilter">
<property name="checks" value="RegexpHeader"/>
<property name="files" value=".*[\\/]org[\\/]jackhuang[\\/]hmcl[\\/]ui[\\/]skin[\\/].*"/>
</module>
<module name="SuppressionSingleFilter">
<property name="checks" value="RegexpHeader"/>
<property name="files" value=".*[\\/](SwingFXUtils|ScrollUtils|TypeUtils).java"/>
</module>
<module name="RegexpHeader">
<property name="headerFile" value="${licenseHeaderFile}"/>
<property name="charset" value="UTF-8"/>
<message key="header.mismatch"
value="Line does not match expected header line of ''{0}''. Please add the missing copyright header."/>
</module>
</module>
-17
View File
@@ -1,17 +0,0 @@
^/\*$
^ \* Hello Minecraft! Launcher$
^ \* Copyright \(C\) \d{4}(-\d{4})? +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/>\.$
^ \*/$
+1 -1
View File
@@ -15,4 +15,4 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
versionRoot=3.13
versionRoot=3.12
+4 -4
View File
@@ -6,10 +6,9 @@ simple-png-javafx = "0.3.0"
gson = "2.13.2"
toml4j = "0.7.2"
xz = "1.11"
lz4 = "1.10.4.1"
fx-gson = "5.0.0"
constant-pool-scanner = "1.2"
hello-nbt = "0.2.0"
opennbt = "1.5"
nanohttpd = "2.3.1"
jsoup = "1.21.2"
chardet = "2.5.0"
@@ -18,6 +17,7 @@ jna = "5.18.1"
pci-ids = "0.4.0"
java-info = "1.0"
authlib-injector = "1.2.7"
lwjgl-unsafe-agent = "2.0"
monet-fx = "0.4.0"
terracotta = "0.4.2"
nayuki-qrcodegen = "1.8.0"
@@ -38,10 +38,9 @@ simple-png-javafx = { module = "org.glavo:simple-png-javafx", version.ref = "sim
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
toml = { module = "com.moandjiezana.toml:toml4j", version.ref = "toml4j" }
xz = { module = "org.tukaani:xz", version.ref = "xz" }
lz4 = { module = "org.glavo:lz4-java", version.ref = "lz4" }
fx-gson = { module = "org.hildan.fxgson:fx-gson", version.ref = "fx-gson" }
constant-pool-scanner = { module = "org.jenkins-ci:constant-pool-scanner", version.ref = "constant-pool-scanner" }
hello-nbt = { module = "org.glavo:HelloNBT", version.ref = "hello-nbt" }
opennbt = { module = "com.github.steveice10:opennbt", version.ref = "opennbt" }
nanohttpd = { module = "org.nanohttpd:nanohttpd", version.ref = "nanohttpd" }
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
chardet = { module = "org.glavo:chardet", version.ref = "chardet" }
@@ -51,6 +50,7 @@ jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna" }
pci-ids = { module = "org.glavo:pci-ids", version.ref = "pci-ids" }
java-info = { module = "org.glavo:java-info", version.ref = "java-info" }
authlib-injector = { module = "org.glavo.hmcl:authlib-injector", version.ref = "authlib-injector" }
lwjgl-unsafe-agent = { module = "org.glavo:lwjgl-unsafe-agent", version.ref = "lwjgl-unsafe-agent" }
monet-fx = { module = "org.glavo:MonetFX", version.ref = "monet-fx" }
nayuki-qrcodegen = { module = "io.nayuki:qrcodegen", version.ref = "nayuki-qrcodegen" }