Compare commits

...
28 Commits
Author SHA1 Message Date
Glavo f3a5b2a99d 支持 26w14a (#5892) 2026-04-02 00:02:08 +08:00
neveler eac2740ea8 优化关于页组件间距使整体风格更加统一协调 (#5861) 2026-03-30 21:17:27 +08:00
a06da53ec9 修复清理游戏资源 / 库操作在 FXThread 运行的问题 (#5301)
Co-authored-by: Glavo <zjx001202@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-30 21:04:08 +08:00
Xirren 765ea24d22 优化手动更新URL (#5880) 2026-03-30 20:40:50 +08:00
Glavo bd24ab37f5 Bump XZ for Java to 1.12 (#5883) 2026-03-30 20:40:32 +08:00
Glavo c689272c42 Bump lwjgl-unsafe-agent to 2.0 (#5882) 2026-03-30 20:27:11 +08:00
952cc5dd37 [Feature] 允许设置游戏内容默认下载源 (#5781)
Resolves #5594

Co-authored-by: Glavo <zjx001202@gmail.com>
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2026-03-29 22:07:20 +08:00
辞庐andGlavo 356df5d630 更新 amdgpu.ids 文件 (#5316)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-03-29 21:58:43 +08:00
Damon Lu 0e433c5f4b 启动时检测 Wine 环境并进行警告 (#5518) 2026-03-29 21:56:44 +08:00
Glavo a28509de85 支持加载 SVG 图片 (#5484) 2026-03-29 21:29:25 +08:00
3gf8jv4dv e45601ecda chore: unify version type and release channel in english locale (#5548) 2026-03-29 21:02:54 +08:00
辞庐andGlavo 5bbf5cbfc8 启动时对会丢失游戏数据的情况添加 5 秒确认 (#5738)
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-03-29 21:00:44 +08:00
afb98c20ce 优化 update.note.dev 英语换行 (#5876)
Co-authored-by: Glavo <zjx001202@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-29 20:52:31 +08:00
辞庐 6b2bc4606b fix: JFXDialog 弹窗过程中模糊 (#5877)
resolves #5073
2026-03-29 20:43:58 +08:00
mineDiamondandGlavo 6b57cec576 feat: 为自定义颜色选择对话框添加显式的确认和取消按钮 (#5775)
close #5767

Co-authored-by: Glavo <zjx001202@gmail.com>
2026-03-29 20:43:26 +08:00
Glavo 83111b83f2 更新 launch.advice.lwjgl_3_4_1 文言文译文 (#5874) 2026-03-29 02:44:29 +08:00
Glavo 420e337c8c Update LWJGL advice message (#5873) 2026-03-29 02:18:20 +08:00
Glavoand3gf8jv4dv 028e43c959 通过 Java Agent 优化 LWJGL 3.4.1 性能 (#5870)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
2026-03-29 01:35:07 +08:00
ef01acca6d feat: 下载页面折叠快照 Minecraft 版本 (#5601)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
2026-03-27 22:58:49 +08:00
110d5f5630 更新更新渠道选择选项 (#5703)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
Co-authored-by: Glavo <zjx001202@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-27 21:41:54 +08:00
neveler 94497a073d 种子地图、要塞地图、下届要塞地图和末地城地图支持 26.1 (#5860) 2026-03-27 21:06:36 +08:00
mineDiamond ff540a95ee refactor: 将 Datapack 重命名为 DataPack (#5785) 2026-03-26 21:26:18 +08:00
辞庐 ff6a32cdc3 fix: SettingsMap 缺少 toString 方法 (#5857) 2026-03-26 21:19:06 +08:00
Glavo 969644635a Bump HelloNBT to 0.3.0 (#5855) 2026-03-25 18:12:40 +08:00
Glavo a053736e95 Bump twelvemonkeys to 3.13.1 (#5854) 2026-03-25 18:07:58 +08:00
Glavo 879f82c462 Bump kala-compress to 1.27.1-3 (#5851) 2026-03-25 17:45:09 +08:00
Glavo e2d8d9f93f 更新文言文翻译 (#5845) 2026-03-24 21:39:53 +08:00
Glavo da4ed04697 Update default Java version to JAVA_25 (#5841) 2026-03-24 20:35:02 +08:00
52 changed files with 814 additions and 311 deletions

No files matched your search

+3
View File
@@ -57,6 +57,7 @@ dependencies {
implementation(project(":HMCLBoot"))
implementation("libs:JFoenix")
implementation(libs.twelvemonkeys.imageio.webp)
implementation(libs.fxsvgimage)
implementation(libs.java.info)
implementation(libs.monet.fx)
implementation(libs.nayuki.qrcodegen)
@@ -66,6 +67,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 +157,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")
@@ -23,21 +23,13 @@ import com.jfoenix.controls.events.JFXDialogEvent;
import com.jfoenix.converters.DialogTransitionConverter;
import com.jfoenix.effects.JFXDepthManager;
import com.jfoenix.transitions.CachedTransition;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.animation.Transition;
import javafx.animation.*;
import javafx.beans.DefaultProperty;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableObjectProperty;
import javafx.css.Styleable;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleableProperty;
import javafx.css.*;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
@@ -379,6 +371,14 @@ public class JFXDialog extends StackPane {
setCycleDuration(Duration.seconds(0.4));
setDelay(Duration.ZERO);
}
@Override
protected void starting() {
}
@Override
protected void stopping() {
}
}
/***************************************************************************
@@ -40,12 +40,16 @@ import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.stage.*;
import javafx.util.Duration;
import org.glavo.monetfx.ColorScheme;
import org.jackhuang.hmcl.setting.StyleSheets;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/**
* @author Shadi Shaheen
*/
@@ -78,7 +82,7 @@ public class JFXCustomColorPickerDialog extends StackPane {
// create JFX Decorator
pickerDecorator = new JFXDecorator(dialog, this, false, false, false);
pickerDecorator.setOnCloseButtonAction(this::updateColor);
pickerDecorator.setOnCloseButtonAction(this::close);
pickerDecorator.setPickOnBounds(false);
customScene = new Scene(pickerDecorator, Color.TRANSPARENT);
StyleSheets.init(customScene);
@@ -153,6 +157,22 @@ public class JFXCustomColorPickerDialog extends StackPane {
paraTransition.play();
});
container.getChildren().add(tabs);
HBox actionsHBox = new HBox();
actionsHBox.getStyleClass().add("jfx-color-dialog-actions");
JFXButton acceptButton = new JFXButton(i18n("button.ok"));
acceptButton.setOnAction(event -> updateColor());
acceptButton.getStyleClass().add("jfx-color-dialog-accept");
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.setOnAction(event -> close());
cancelButton.getStyleClass().add("jfx-color-dialog-cancel");
actionsHBox.getChildren().addAll(acceptButton, cancelButton);
container.getChildren().add(actionsHBox);
initRun = () -> {
// change tabs labels font color according to the selected color
pane.backgroundProperty().addListener((o, oldVal, newVal) -> {
@@ -232,7 +252,13 @@ public class JFXCustomColorPickerDialog extends StackPane {
hexField.focusColorProperty().bind(Bindings.createObjectBinding(() -> {
return pane.getBackground().getFills().get(0).getFill();
}, pane.backgroundProperty()));
acceptButton.textFillProperty().bind(Bindings.createObjectBinding(() -> {
Color fill = (Color) pane.getBackground().getFills().get(0).getFill();
return ColorScheme.newBuilder(Themes.getColorScheme())
.setPrimaryColorSeed(fill)
.build()
.getPrimary();
}, pane.backgroundProperty()));
((Pane) pickerDecorator.lookup(".jfx-decorator-buttons-container")).backgroundProperty()
.bind(Bindings.createObjectBinding(() -> {
@@ -262,9 +288,6 @@ public class JFXCustomColorPickerDialog extends StackPane {
});
};
container.getChildren().add(tabs);
this.getChildren().add(container);
this.setPadding(new Insets(0));
@@ -25,6 +25,7 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import javax.swing.JOptionPane;
import java.io.IOException;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
@@ -49,6 +50,8 @@ public final class EntryPoint {
createHMCLDirectories();
LOG.start(Metadata.HMCL_CURRENT_DIRECTORY.resolve("logs"));
checkWine();
setupJavaFXVMOptions();
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
@@ -220,6 +223,20 @@ public final class EntryPoint {
}
}
private static void checkWine() {
if (OperatingSystem.isRunningUnderWine()) {
SwingUtils.initLookAndFeel();
LOG.warning("HMCL is running under Wine or its distributions!");
int result = JOptionPane.showOptionDialog(null, i18n("fatal.wine_warning"), i18n("message.warning"), JOptionPane.OK_CANCEL_OPTION,
JOptionPane.WARNING_MESSAGE, null, null, null);
if (result == JOptionPane.CANCEL_OPTION || result == JOptionPane.CLOSED_OPTION) {
exit(1);
}
}
}
private static void addEnableNativeAccess() {
if (JavaRuntime.CURRENT_VERSION > 21) {
try {
@@ -17,35 +17,36 @@
*/
package org.jackhuang.hmcl;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ObservableBooleanValue;
import javafx.geometry.Rectangle2D;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.input.Clipboard;
import javafx.scene.input.DataFormat;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.setting.SambaException;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.CrashReporter;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.NativeUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemInfo;
import org.jackhuang.hmcl.util.platform.*;
import java.io.File;
import java.io.IOException;
@@ -65,8 +66,8 @@ import java.util.concurrent.TimeUnit;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class Launcher extends Application {
public static final CookieManager COOKIE_MANAGER = new CookieManager();
@@ -111,7 +112,7 @@ public final class Launcher extends Application {
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
&& ConfigHolder.isNewlyCreated()
&& System.getProperty("user.dir").startsWith("/private/var/folders/")) {
if (showAlert(AlertType.WARNING, i18n("fatal.mac_app_translocation"), ButtonType.YES, ButtonType.NO) == ButtonType.NO)
if (!confirmWithCountdown(AlertType.WARNING, i18n("fatal.mac_app_translocation"), 5))
return;
} else {
checkConfigInTempDir();
@@ -182,6 +183,27 @@ public final class Launcher extends Application {
return new Alert(alertType, contentText, buttons).showAndWait().orElse(null);
}
private static boolean confirmWithCountdown(Alert.AlertType alertType, String contentText, int seconds) {
Alert alert = new Alert(alertType, contentText, ButtonType.YES, ButtonType.NO);
Button okButton = (Button) alert.getDialogPane().lookupButton(ButtonType.YES);
okButton.setDisable(true);
KeyFrame[] keyFrames = new KeyFrame[seconds + 1];
for (int i = 0; i < seconds; i++) {
keyFrames[i] = new KeyFrame(Duration.seconds(i),
new KeyValue(okButton.textProperty(), i18n("button.ok.countdown", seconds - i)));
}
keyFrames[seconds] = new KeyFrame(Duration.seconds(seconds),
new KeyValue(okButton.textProperty(), i18n("button.ok")),
new KeyValue(okButton.disableProperty(), false));
Timeline timeline = new Timeline(keyFrames);
alert.setOnShown(e -> timeline.play());
alert.setOnCloseRequest(e -> timeline.stop());
return alert.showAndWait().orElse(null) == ButtonType.YES;
}
private static boolean isConfigInTempDir() {
String configPath = ConfigHolder.configLocation().toString();
@@ -221,7 +243,7 @@ public final class Launcher extends Application {
private static void checkConfigInTempDir() {
if (ConfigHolder.isNewlyCreated() && isConfigInTempDir()
&& showAlert(AlertType.WARNING, i18n("fatal.config_in_temp_dir"), ButtonType.YES, ButtonType.NO) == ButtonType.NO) {
&& !confirmWithCountdown(AlertType.WARNING, i18n("fatal.config_in_temp_dir"), 5)) {
EntryPoint.exit(0);
}
}
@@ -49,6 +49,7 @@ public final class Metadata {
public static final String ABOUT_URL = PUBLISH_URL + "/about";
public static final String DOWNLOAD_URL = PUBLISH_URL + "/download";
public static final String HMCL_UPDATE_URL = System.getProperty("hmcl.update_source.override", PUBLISH_URL + "/api/update_link");
public static final String MANUAL_UPDATE_URL = "https://github.com/HMCL-dev/HMCL/releases";
public static final String DOCS_URL = "https://docs.hmcl.net";
public static final String CONTACT_URL = DOCS_URL + "/help.html";
@@ -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;
}
}
@@ -310,7 +310,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
Optional<Path> iconFile = getVersionIconFile(id);
if (iconFile.isPresent()) {
try {
return FXUtils.loadImage(iconFile.get());
return FXUtils.loadImage(iconFile.get(), 64, 64, true, true);
} catch (Exception e) {
LOG.warning("Failed to load version icon of " + id, e);
}
@@ -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(
@@ -567,6 +567,21 @@ public final class Config extends ObservableSetting {
this.versionListSource.set(versionListSource);
}
@SerializedName("defaultAddonSource")
private final StringProperty defaultAddonSource = new SimpleStringProperty("modrinth");
public StringProperty defaultAddonSourceProperty() {
return defaultAddonSource;
}
public String getDefaultAddonSource() {
return defaultAddonSource.get();
}
public void setDefaultAddonSource(String defaultAddonSource) {
this.defaultAddonSource.set(defaultAddonSource);
}
@SerializedName("hasProxy")
private final BooleanProperty hasProxy = new SimpleBooleanProperty();
@@ -689,6 +704,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")
@@ -235,8 +235,8 @@ public final class FXUtils {
public static final String DEFAULT_MONOSPACE_FONT = OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "Consolas" : "Monospace";
public static final List<String> IMAGE_EXTENSIONS = Lang.immutableListOf(
"png", "jpg", "jpeg", "bmp", "gif", "webp", "apng"
public static final List<String> IMAGE_EXTENSIONS = List.of(
"png", "jpg", "jpeg", "bmp", "gif", "webp", "svg", "apng"
);
private static final Map<String, Image> builtinImageCache = new ConcurrentHashMap<>();
@@ -30,9 +30,6 @@ import org.jackhuang.hmcl.ui.SVG;
public class LineButton extends LineButtonBase {
private static final String DEFAULT_STYLE_CLASS = "line-button";
private static final int IDX_TRAILING_TEXT = IDX_TRAILING;
private static final int IDX_TRAILING_ICON = IDX_TRAILING + 1;
public static LineButton createNavigationButton() {
var button = new LineButton();
button.setTrailingIcon(SVG.ARROW_FORWARD);
@@ -50,7 +47,14 @@ public class LineButton extends LineButtonBase {
public LineButton() {
getStyleClass().add(DEFAULT_STYLE_CLASS);
container.setMouseTransparent(true);
}
protected int getTrailingTextIndex() {
return IDX_TRAILING;
}
protected int getTrailingIconIndex() {
return getTrailingTextIndex() + 1;
}
private ObjectProperty<EventHandler<ActionEvent>> onAction;
@@ -110,12 +114,13 @@ public class LineButton extends LineButtonBase {
trailingTextLabel = new Label();
trailingTextLabel.getStyleClass().add("trailing-label");
trailingTextLabel.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
trailingTextLabel.setMouseTransparent(true);
}
trailingTextLabel.setText(message);
setNode(IDX_TRAILING_TEXT, trailingTextLabel);
setNode(getTrailingTextIndex(), trailingTextLabel);
} else if (trailingTextLabel != null) {
trailingTextLabel.setText("");
setNode(IDX_TRAILING_TEXT, null);
setNode(getTrailingTextIndex(), null);
}
}
};
@@ -149,7 +154,7 @@ public class LineButton extends LineButtonBase {
@Override
protected void invalidated() {
setNode(IDX_TRAILING_ICON, get());
setNode(getTrailingIconIndex(), get());
}
};
@@ -171,6 +176,7 @@ public class LineButton extends LineButtonBase {
public void setTrailingIcon(SVG rightIcon, double size) {
Node rightIconNode = rightIcon.createIcon(size);
rightIconNode.getStyleClass().add("trailing-icon");
rightIconNode.setMouseTransparent(true);
setTrailingIcon(rightIconNode);
}
}
@@ -64,6 +64,7 @@ public abstract class LineComponent extends StackPane implements NoPaddingCompon
this.container = new HBox(SPACING);
container.getStyleClass().add("line-component-container");
container.setAlignment(Pos.CENTER_LEFT);
container.setPickOnBounds(true);
this.titleLabel = new Label();
titleLabel.getStyleClass().add("title-label");
@@ -43,7 +43,7 @@ import java.util.function.Function;
import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition;
/// @author Glavo
public final class LineSelectButton<T> extends LineButton {
public class LineSelectButton<T> extends LineButton {
private static final String DEFAULT_STYLE_CLASS = "line-select-button";
private static final PseudoClass SELECTED_PSEUDO_CLASS = PseudoClass.getPseudoClass("selected");
@@ -19,9 +19,17 @@ package org.jackhuang.hmcl.ui.image;
import com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.scene.SnapshotParameters;
import javafx.scene.image.Image;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.WritableImage;
import javafx.scene.paint.Color;
import org.girod.javafx.svgimage.LoaderParameters;
import org.girod.javafx.svgimage.SVGImage;
import org.girod.javafx.svgimage.SVGLoader;
import org.girod.javafx.svgimage.ScaleQuality;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.image.apng.Png;
import org.jackhuang.hmcl.ui.image.apng.argb8888.Argb8888Bitmap;
import org.jackhuang.hmcl.ui.image.apng.argb8888.Argb8888BitmapSequence;
@@ -39,7 +47,9 @@ import javax.imageio.stream.ImageInputStream;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.regex.Pattern;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -73,6 +83,46 @@ public final class ImageUtils {
return SwingFXUtils.toFXImage(bufferedImage, requestedWidth, requestedHeight, preserveRatio, smooth);
};
public static final ImageLoader SVG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
String content = new String(input.readAllBytes(), StandardCharsets.UTF_8);
LoaderParameters parameters = new LoaderParameters();
parameters.autoStartAnimations = false;
SVGImage image;
if (Platform.isFxApplicationThread()) {
image = SVGLoader.load(content, parameters);
} else {
// TODO: Currently, SVGLoader.load(...) requires the javafx.swing module if it operates on a non-JavaFX thread.
image = CompletableFuture.supplyAsync(
() -> SVGLoader.load(content, parameters),
Schedulers.javafx()
).get();
}
if (image == null)
throw new IOException("Failed to load SVG image");
var snapshotParameters = new SnapshotParameters();
snapshotParameters.setFill(Color.TRANSPARENT);
if (requestedWidth <= 0. || requestedHeight <= 0.) {
return image.toImage(snapshotParameters);
}
double scaleX = requestedWidth / image.getScaledWidth();
double scaleY = requestedHeight / image.getScaledHeight();
if (preserveRatio || scaleX == scaleY) {
double scale = Math.min(scaleX, scaleY);
return image.scale(scale).toImage(snapshotParameters);
} else {
// FIXME: Use DEFAULT_SVG_SNAPSHOT_PARAMS
return image.toImageScaled(ScaleQuality.RENDER_QUALITY, scaleX, scaleY);
}
};
public static final ImageLoader APNG = (input, requestedWidth, requestedHeight, preserveRatio, smooth) -> {
if (!"true".equals(System.getProperty("hmcl.experimental.apng", "true")))
return DEFAULT.load(input, requestedWidth, requestedHeight, preserveRatio, smooth);
@@ -136,11 +186,13 @@ public final class ImageUtils {
public static final Map<String, ImageLoader> EXT_TO_LOADER = Map.of(
"webp", WEBP,
"svg", SVG,
"apng", APNG
);
public static final Map<String, ImageLoader> CONTENT_TYPE_TO_LOADER = Map.of(
"image/webp", WEBP,
"image/svg+xml", SVG,
"image/apng", APNG
);
@@ -165,6 +217,14 @@ public final class ImageUtils {
&& Arrays.equals(headerBuffer, 8, 12, WEBP_HEADER, 0, 4);
}
private static final byte[] SVG_HEADER = "<svg".getBytes(StandardCharsets.US_ASCII);
// This is currently a simple check, more complex checks can be considered in the future
public static boolean isSVG(byte[] headerBuffer) {
return headerBuffer.length > SVG_HEADER.length
&& Arrays.equals(headerBuffer, 0, SVG_HEADER.length, SVG_HEADER, 0, SVG_HEADER.length);
}
private static final byte[] PNG_HEADER = {
(byte) 0x89, (byte) 0x50, (byte) 0x4e, (byte) 0x47,
(byte) 0x0d, (byte) 0x0a, (byte) 0x1a, (byte) 0x0a,
@@ -232,6 +292,8 @@ public final class ImageUtils {
return WEBP;
if (isApng(headerBuffer))
return APNG;
if (isSVG(headerBuffer))
return SVG;
return null;
}
@@ -18,10 +18,8 @@
package org.jackhuang.hmcl.ui.main;
import com.google.gson.*;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.theme.Themes;
@@ -30,6 +28,7 @@ import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.LineButton;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import java.io.IOException;
@@ -38,11 +37,18 @@ import java.io.InputStream;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class AboutPage extends StackPane {
public final class AboutPage extends SpinnerPane {
private final WeakListenerHolder holder = new WeakListenerHolder();
public AboutPage() {
VBox content = new VBox();
content.getStyleClass().add("spinner-pane-content");
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setFitToWidth(true);
FXUtils.smoothScrolling(scrollPane);
setContent(scrollPane);
ComponentList about = new ComponentList();
{
var launcher = LineButton.createExternalLinkButton(Metadata.PUBLISH_URL);
@@ -84,27 +90,16 @@ public final class AboutPage extends StackPane {
legal.getContent().setAll(copyright, claim, openSource);
}
VBox content = new VBox(16);
content.setPadding(new Insets(10));
content.getChildren().setAll(
ComponentList.createComponentListTitle(i18n("about")),
about,
ComponentList.createComponentListTitle(i18n("about.thanks_to")),
thanks,
ComponentList.createComponentListTitle(i18n("about.dependency")),
deps,
ComponentList.createComponentListTitle(i18n("about.legal")),
legal
);
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setFitToWidth(true);
FXUtils.smoothScrolling(scrollPane);
getChildren().setAll(scrollPane);
}
private static Image loadImage(String url) {
@@ -85,7 +85,13 @@ public class DownloadSettingsPage extends StackPane {
downloadSourcePane.setItems(DownloadProviders.DIRECT_PROVIDERS.keySet());
downloadSourcePane.valueProperty().bindBidirectional(config().downloadTypeProperty());
downloadSource.getContent().setAll(autoChooseDownloadSource, versionListSourcePane, downloadSourcePane);
var defaultAddonSourcePane = new LineSelectButton<String>();
defaultAddonSourcePane.setTitle(i18n("settings.launcher.default_addon_source"));
defaultAddonSourcePane.setConverter(key -> I18n.i18n("mods." + key));
defaultAddonSourcePane.setItems("modrinth", "curseforge");
defaultAddonSourcePane.valueProperty().bindBidirectional(config().defaultAddonSourceProperty());
downloadSource.getContent().setAll(autoChooseDownloadSource, versionListSourcePane, downloadSourcePane, defaultAddonSourcePane);
}
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.download_source")), downloadSource);
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.ui.main;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.theme.Themes;
@@ -37,9 +36,7 @@ public class FeedbackPage extends SpinnerPane {
public FeedbackPage() {
VBox content = new VBox();
content.setPadding(new Insets(10));
content.setSpacing(10);
content.setFillWidth(true);
content.getStyleClass().add("spinner-pane-content");
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setFitToWidth(true);
FXUtils.smoothScrolling(scrollPane);
@@ -84,8 +81,5 @@ public class FeedbackPage extends SpinnerPane {
ComponentList.createComponentListTitle(i18n("contact.feedback")),
feedback
);
this.setContent(content);
}
}
@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui.main;
import com.google.gson.annotations.SerializedName;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Metadata;
@@ -42,9 +41,7 @@ public class HelpPage extends SpinnerPane {
public HelpPage() {
content = new VBox();
content.setPadding(new Insets(10));
content.setSpacing(10);
content.setFillWidth(true);
content.getStyleClass().add("spinner-pane-content");
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setFitToWidth(true);
FXUtils.smoothScrolling(scrollPane);
@@ -18,18 +18,18 @@
package org.jackhuang.hmcl.ui.main;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXRadioButton;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.StringProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.Cursor;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.*;
import javafx.scene.text.TextAlignment;
import org.jackhuang.hmcl.Metadata;
@@ -69,13 +69,9 @@ import java.util.zip.ZipOutputStream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class SettingsPage extends ScrollPane {
@SuppressWarnings("FieldCanBeLocal")
private final ToggleGroup updateChannelGroup;
@SuppressWarnings("FieldCanBeLocal")
private final InvalidationListener updateListener;
@@ -122,82 +118,59 @@ public final class SettingsPage extends ScrollPane {
settingsPane.getContent().add(sponsorPane);
}
ObjectProperty<UpdateChannel> updateChannel;
{
ComponentSublist updatePane = new ComponentSublist();
JFXButton updateButton = new JFXButton();
updateButton.setGraphic(SVG.UPDATE.createIcon(20));
updateButton.setOnAction(e -> onUpdate());
updateButton.setPadding(Insets.EMPTY);
FXUtils.installFastTooltip(updateButton, i18n("update.tooltip"));
var updatePane = new LineSelectButton<UpdateChannel>() {
{
getStyleClass().add("update-pane");
setNode(IDX_TRAILING, updateButton);
}
@Override
protected int getTrailingTextIndex() {
return LineComponent.IDX_TRAILING + 1;
}
};
updateChannel = updatePane.valueProperty();
updatePane.setTitle(i18n("update"));
updatePane.setHasSubtitle(true);
updatePane.setValue(UpdateChannel.getChannel());
final Label lblUpdate;
final Label lblUpdateSub;
{
VBox headerLeft = new VBox();
updatePane.setConverter(channel -> i18n("update.channel." + channel.channelName));
updatePane.setItems(List.of(UpdateChannel.STABLE, UpdateChannel.DEVELOPMENT));
updatePane.setDescriptionConverter(channel -> i18n("update.note." + channel.channelName));
lblUpdate = new Label(i18n("update"));
lblUpdate.getStyleClass().add("title-label");
lblUpdateSub = new Label();
lblUpdateSub.getStyleClass().add("subtitle-label");
headerLeft.getChildren().setAll(lblUpdate, lblUpdateSub);
updatePane.setHeaderLeft(headerLeft);
}
final StringProperty lblUpdateSubProperty = updatePane.subtitleProperty();
{
JFXButton btnUpdate = FXUtils.newToggleButton4(SVG.UPDATE, 20);
btnUpdate.setOnAction(e -> onUpdate());
FXUtils.installFastTooltip(btnUpdate, i18n("update.tooltip"));
updateListener = any -> {
btnUpdate.setVisible(UpdateChecker.isOutdated());
boolean outdated = UpdateChecker.isOutdated();
updateButton.setVisible(outdated);
updateButton.setManaged(outdated);
updatePane.pseudoClassStateChanged(PseudoClass.getPseudoClass("active"), outdated);
if (UpdateChecker.isOutdated()) {
lblUpdateSub.setText(i18n("update.newest_version", UpdateChecker.getLatestVersion().getVersion()));
lblUpdateSub.getStyleClass().setAll("update-label");
lblUpdate.setText(i18n("update.found"));
lblUpdate.getStyleClass().setAll("update-label");
lblUpdateSubProperty.set(i18n("update.newest_version", UpdateChecker.getLatestVersion().getVersion()));
} else if (UpdateChecker.isCheckingUpdate()) {
lblUpdateSub.setText(i18n("update.checking"));
lblUpdateSub.getStyleClass().setAll("subtitle-label");
lblUpdate.setText(i18n("update"));
lblUpdate.getStyleClass().setAll("title-label");
lblUpdateSubProperty.set(i18n("update.checking"));
} else {
lblUpdateSub.setText(i18n("update.latest"));
lblUpdateSub.getStyleClass().setAll("subtitle-label");
lblUpdate.setText(i18n("update"));
lblUpdate.getStyleClass().setAll("title-label");
lblUpdateSubProperty.set(i18n("update.latest"));
}
};
UpdateChecker.latestVersionProperty().addListener(new WeakInvalidationListener(updateListener));
UpdateChecker.outdatedProperty().addListener(new WeakInvalidationListener(updateListener));
UpdateChecker.checkingUpdateProperty().addListener(new WeakInvalidationListener(updateListener));
updateListener.invalidated(null);
updatePane.setHeaderRight(btnUpdate);
}
{
VBox content = new VBox(12);
content.setPadding(new Insets(8, 0, 0, 0));
updateChannelGroup = new ToggleGroup();
JFXRadioButton chkUpdateStable = new JFXRadioButton(i18n("update.channel.stable"));
chkUpdateStable.setUserData(UpdateChannel.STABLE);
chkUpdateStable.setToggleGroup(updateChannelGroup);
JFXRadioButton chkUpdateDev = new JFXRadioButton(i18n("update.channel.dev"));
chkUpdateDev.setUserData(UpdateChannel.DEVELOPMENT);
chkUpdateDev.setToggleGroup(updateChannelGroup);
Label noteWrapper = new Label(i18n("update.note"));
VBox.setMargin(noteWrapper, new Insets(8, 0, 0, 0));
content.getChildren().setAll(chkUpdateStable, chkUpdateDev, noteWrapper);
updatePane.getContent().add(content);
}
settingsPane.getContent().add(updatePane);
}
@@ -207,8 +180,6 @@ public final class SettingsPage extends ScrollPane {
previewPane.setSubtitle(i18n("update.preview.subtitle"));
previewPane.selectedProperty().bindBidirectional(config().acceptPreviewUpdateProperty());
ObjectProperty<UpdateChannel> updateChannel = selectedItemPropertyFor(updateChannelGroup, UpdateChannel.class);
updateChannel.set(UpdateChannel.getChannel());
InvalidationListener checkUpdateListener = e -> {
UpdateChecker.requestCheckUpdate(updateChannel.get(), previewPane.isSelected());
};
@@ -289,6 +260,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();
@@ -22,7 +22,7 @@ import javafx.collections.ObservableList;
import javafx.scene.control.Skin;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.mod.Datapack;
import org.jackhuang.hmcl.mod.DataPack;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
@@ -44,45 +44,45 @@ import java.util.regex.Pattern;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.DatapackInfoObject> implements WorldManagePage.WorldRefreshable {
public final class DataPackListPage extends ListPageBase<DataPackListPageSkin.DataPackInfoObject> implements WorldManagePage.WorldRefreshable {
private final World world;
private final Datapack datapack;
private final DataPack dataPack;
final BooleanProperty readOnly;
public DatapackListPage(WorldManagePage worldManagePage) {
public DataPackListPage(WorldManagePage worldManagePage) {
world = worldManagePage.getWorld();
datapack = new Datapack(world.getFile().resolve("datapacks"));
setItems(MappedObservableList.create(datapack.getPacks(), DatapackListPageSkin.DatapackInfoObject::new));
dataPack = new DataPack(world.getFile().resolve("datapacks"));
setItems(MappedObservableList.create(dataPack.getPacks(), DataPackListPageSkin.DataPackInfoObject::new));
readOnly = worldManagePage.readOnlyProperty();
FXUtils.applyDragListener(this, it -> Objects.equals("zip", FileUtils.getExtension(it)),
this::installMultiDatapack, this::refresh);
this::installMultiDataPack, this::refresh);
refresh();
}
private void installMultiDatapack(List<Path> datapackPath) {
datapackPath.forEach(this::installSingleDatapack);
private void installMultiDataPack(List<Path> dataPackPath) {
dataPackPath.forEach(this::installSingleDataPack);
if (readOnly.get()) {
Controllers.showToast(i18n("datapack.reload.toast"));
}
}
private void installSingleDatapack(Path datapack) {
private void installSingleDataPack(Path dataPack) {
try {
this.datapack.installPack(datapack, world.getGameVersion());
this.dataPack.installPack(dataPack, world.getGameVersion());
} catch (IOException | IllegalArgumentException e) {
LOG.warning("Unable to parse datapack file " + datapack, e);
LOG.warning("Unable to parse datapack file " + dataPack, e);
}
}
@Override
protected Skin<?> createDefaultSkin() {
return new DatapackListPageSkin(this);
return new DataPackListPageSkin(this);
}
public void refresh() {
setLoading(true);
Task.runAsync(datapack::loadFromDir)
Task.runAsync(dataPack::loadFromDir)
.withRunAsync(Schedulers.javafx(), () -> setLoading(false))
.start();
}
@@ -94,18 +94,18 @@ public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.Da
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
if (res != null) {
installMultiDatapack(res);
installMultiDataPack(res);
}
datapack.loadFromDir();
dataPack.loadFromDir();
}
void removeSelected(ObservableList<DatapackListPageSkin.DatapackInfoObject> selectedItems) {
void removeSelected(ObservableList<DataPackListPageSkin.DataPackInfoObject> selectedItems) {
selectedItems.stream()
.map(DatapackListPageSkin.DatapackInfoObject::getPackInfo)
.map(DataPackListPageSkin.DataPackInfoObject::getPackInfo)
.forEach(pack -> {
try {
datapack.deletePack(pack);
dataPack.deletePack(pack);
} catch (IOException e) {
// Fail to remove mods if the game is running or the datapack is absent.
LOG.warning("Failed to delete datapack \"" + pack.getId() + "\"", e);
@@ -113,23 +113,23 @@ public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.Da
});
}
void enableSelected(ObservableList<DatapackListPageSkin.DatapackInfoObject> selectedItems) {
void enableSelected(ObservableList<DataPackListPageSkin.DataPackInfoObject> selectedItems) {
selectedItems.stream()
.map(DatapackListPageSkin.DatapackInfoObject::getPackInfo)
.map(DataPackListPageSkin.DataPackInfoObject::getPackInfo)
.forEach(pack -> pack.setActive(true));
}
void disableSelected(ObservableList<DatapackListPageSkin.DatapackInfoObject> selectedItems) {
void disableSelected(ObservableList<DataPackListPageSkin.DataPackInfoObject> selectedItems) {
selectedItems.stream()
.map(DatapackListPageSkin.DatapackInfoObject::getPackInfo)
.map(DataPackListPageSkin.DataPackInfoObject::getPackInfo)
.forEach(pack -> pack.setActive(false));
}
void openDataPackFolder() {
FXUtils.openFolder(datapack.getPath());
FXUtils.openFolder(dataPack.getPath());
}
@NotNull Predicate<DatapackListPageSkin.DatapackInfoObject> updateSearchPredicate(String queryString) {
@NotNull Predicate<DataPackListPageSkin.DataPackInfoObject> updateSearchPredicate(String queryString) {
if (queryString.isBlank()) {
return dataPack -> true;
}
@@ -42,7 +42,7 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import org.jackhuang.hmcl.mod.Datapack;
import org.jackhuang.hmcl.mod.DataPack;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -67,7 +67,7 @@ import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
private final TransitionPane toolbarPane;
private final HBox searchBar;
@@ -75,8 +75,8 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
private final HBox selectingToolbar;
InvalidationListener updateBarByStateWeakListener;
private final JFXListView<DatapackInfoObject> listView;
private final FilteredList<DatapackInfoObject> filteredList;
private final JFXListView<DataPackInfoObject> listView;
private final FilteredList<DataPackInfoObject> filteredList;
private final BooleanProperty isSearching = new SimpleBooleanProperty(false);
private final BooleanProperty isSelecting = new SimpleBooleanProperty(false);
@@ -85,7 +85,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
private static final AtomicInteger lastShiftClickIndex = new AtomicInteger(-1);
final Consumer<Integer> toggleSelect;
DatapackListPageSkin(DatapackListPage skinnable) {
DataPackListPageSkin(DataPackListPage skinnable) {
super(skinnable);
StackPane pane = new StackPane();
@@ -181,7 +181,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
ComponentList.setVgrow(center, Priority.ALWAYS);
center.loadingProperty().bind(skinnable.loadingProperty());
listView.setCellFactory(x -> new DatapackInfoListCell(listView, getSkinnable().readOnly));
listView.setCellFactory(x -> new DataPackInfoListCell(listView, getSkinnable().readOnly));
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
this.listView.setItems(filteredList);
@@ -216,13 +216,13 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
}
}
static class DatapackInfoObject extends RecursiveTreeObject<DatapackInfoObject> {
static class DataPackInfoObject extends RecursiveTreeObject<DataPackInfoObject> {
private final BooleanProperty activeProperty;
private final Datapack.Pack packInfo;
private final DataPack.Pack packInfo;
private SoftReference<CompletableFuture<Image>> iconCache;
DatapackInfoObject(Datapack.Pack packInfo) {
DataPackInfoObject(DataPack.Pack packInfo) {
this.packInfo = packInfo;
this.activeProperty = packInfo.activeProperty();
}
@@ -235,7 +235,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
return packInfo.getDescription().toString();
}
Datapack.Pack getPackInfo() {
DataPack.Pack getPackInfo() {
return packInfo;
}
@@ -270,7 +270,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
}
}
public void loadIcon(ImageContainer imageContainer, @Nullable WeakReference<ObjectProperty<DatapackInfoObject>> current) {
public void loadIcon(ImageContainer imageContainer, @Nullable WeakReference<ObjectProperty<DataPackInfoObject>> current) {
SoftReference<CompletableFuture<Image>> iconCache = this.iconCache;
CompletableFuture<Image> imageFuture;
if (iconCache != null && (imageFuture = iconCache.get()) != null) {
@@ -286,7 +286,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
imageContainer.setImage(FXUtils.newBuiltinImage("/assets/img/unknown_pack.png"));
imageFuture.thenAcceptAsync(image -> {
if (current != null) {
ObjectProperty<DatapackInfoObject> infoObjectProperty = current.get();
ObjectProperty<DataPackInfoObject> infoObjectProperty = current.get();
if (infoObjectProperty == null || infoObjectProperty.get() != this) {
// The current ListCell has already switched to another object
return;
@@ -298,13 +298,13 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
}
}
private final class DatapackInfoListCell extends MDListCell<DatapackInfoObject> {
private final class DataPackInfoListCell extends MDListCell<DataPackInfoObject> {
final JFXCheckBox checkBox = new JFXCheckBox();
ImageContainer imageContainer = new ImageContainer(32);
final TwoLineListItem content = new TwoLineListItem();
BooleanProperty booleanProperty;
DatapackInfoListCell(JFXListView<DatapackInfoObject> listView, BooleanProperty isReadOnlyProperty) {
DataPackInfoListCell(JFXListView<DataPackInfoObject> listView, BooleanProperty isReadOnlyProperty) {
super(listView);
HBox container = new HBox(8);
@@ -326,7 +326,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
}
@Override
protected void updateControl(DatapackInfoObject dataItem, boolean empty) {
protected void updateControl(DataPackInfoObject dataItem, boolean empty) {
if (empty) return;
content.setTitle(dataItem.getTitle());
content.setSubtitle(dataItem.getSubtitle());
@@ -338,7 +338,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
}
}
public void handleSelect(DatapackInfoListCell cell, MouseEvent mouseEvent) {
public void handleSelect(DataPackInfoListCell cell, MouseEvent mouseEvent) {
if (cell.isEmpty()) {
mouseEvent.consume();
return;
@@ -27,16 +27,8 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
@@ -54,11 +46,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.SimpleMultimap;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
@@ -66,14 +54,7 @@ import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -313,25 +294,55 @@ public class DownloadPage extends Control implements DecoratorPage {
}
}
for (String gameVersion : control.versions.keys().stream()
.sorted(Collections.reverseOrder(GameVersionNumber::compare))
.toList()) {
List<RemoteMod.Version> versions = control.versions.get(gameVersion);
if (versions == null || versions.isEmpty()) {
continue;
final class Versions {
static ComponentSublist createSublist(DownloadPage control, String title, List<String> target) {
var sublist = new ComponentSublist(() -> {
ArrayList<ModItem> items = new ArrayList<>();
for (String gv : target) {
List<RemoteMod.Version> versions = control.versions.get(gv);
if (versions != null) {
for (RemoteMod.Version v : versions) {
items.add(new ModItem(control.addon, v, control));
}
}
}
return items;
});
sublist.setTitle(title);
sublist.getStyleClass().add("no-padding");
return sublist;
}
var sublist = new ComponentSublist(() -> {
ArrayList<ModItem> items = new ArrayList<>(versions.size());
for (RemoteMod.Version v : versions) {
items.add(new ModItem(control.addon, v, control));
}
return items;
});
sublist.getStyleClass().add("no-padding");
sublist.setTitle("Minecraft " + gameVersion);
final List<String> releases = new ArrayList<>();
final List<String> snapshots = new ArrayList<>();
}
list.getContent().add(sublist);
TreeMap<GameVersionNumber, Versions> map = new TreeMap<>(Collections.reverseOrder());
for (String version : control.versions.keys()) {
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(version);
GameVersionNumber releaseOfSnapshot = GameVersionNumber.getReleaseOfSnapshot(gameVersion);
GameVersionNumber releaseVersion = Objects.requireNonNullElse(releaseOfSnapshot, gameVersion);
Versions lists = map.computeIfAbsent(releaseVersion, v -> new Versions());
if (releaseOfSnapshot == null) {
lists.releases.add(version);
} else {
lists.snapshots.add(version);
}
}
for (Map.Entry<GameVersionNumber, Versions> entry : map.entrySet()) {
GameVersionNumber gameVersion = entry.getKey();
Versions versions = entry.getValue();
if (!versions.releases.isEmpty())
list.getContent().add(Versions.createSublist(control, i18n("addon.download.title.release", gameVersion), versions.releases));
if (!versions.snapshots.isEmpty())
list.getContent().add(Versions.createSublist(control, i18n("addon.download.title.snapshot", gameVersion), versions.snapshots));
}
});
}
@@ -25,6 +25,7 @@ import org.jackhuang.hmcl.util.i18n.I18n;
import java.util.MissingResourceException;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
@@ -62,17 +63,29 @@ public final class HMCLLocalizedDownloadListPage extends DownloadListPage {
supportChinese.set(true);
boolean supportedCurseForge = CurseForgeRemoteModRepository.isAvailable() && curseForge != null;
downloadSources.setAll("mods.modrinth");
if (CurseForgeRemoteModRepository.isAvailable()) {
if (supportedCurseForge) {
downloadSources.add("mods.curseforge");
}
if (modrinth != null) {
downloadSource.set("mods.modrinth");
} else if (curseForge != null) {
downloadSource.set("mods.curseforge");
if ("curseforge".equalsIgnoreCase(config().getDefaultAddonSource())) {
if (supportedCurseForge) {
downloadSource.set("mods.curseforge");
} else if (modrinth != null) {
downloadSource.set("mods.modrinth");
} else {
throw new AssertionError("Should not be here.");
}
} else {
throw new AssertionError("Should not be here.");
if (modrinth != null) {
downloadSource.set("mods.modrinth");
} else if (supportedCurseForge) {
downloadSource.set("mods.curseforge");
} else {
throw new AssertionError("Should not be here.");
}
}
}
@@ -30,8 +30,10 @@ import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.EventPriority;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
@@ -39,8 +41,10 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.nio.file.Path;
import java.util.Optional;
import java.util.function.Supplier;
@@ -186,15 +190,34 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
}
private void clearLibraries() {
FileUtils.deleteDirectoryQuietly(getProfile().getRepository().getBaseDirectory().resolve("libraries"));
var libraries = getProfile().getRepository().getBaseDirectory().resolve("libraries");
Task.runAsync(Schedulers.io(), () -> {
FileUtils.deleteDirectoryQuietly(libraries);
}).whenComplete(Schedulers.javafx(), (exception) -> {
if (exception != null) {
Controllers.dialog(i18n("message.failed") + "\n" + StringUtils.getStackTrace(exception), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
}
}).start();
}
private void clearAssets() {
HMCLGameRepository baseDirectory = getProfile().getRepository();
FileUtils.deleteDirectoryQuietly(baseDirectory.getBaseDirectory().resolve("assets"));
if (version.get() != null) {
FileUtils.deleteDirectoryQuietly(baseDirectory.getRunDirectory(version.get().getVersion()).resolve("resources"));
}
Path assetsDir = getProfile().getRepository().getBaseDirectory().resolve("assets");
Profile.ProfileVersion currentVersion = version.get();
Path resourcesDir = currentVersion != null
? getProfile().getRepository().getRunDirectory(currentVersion.getVersion()).resolve("resources")
: null;
Task.runAsync(Schedulers.io(), () -> {
FileUtils.deleteDirectoryQuietly(assetsDir);
if (resourcesDir != null) {
FileUtils.deleteDirectoryQuietly(resourcesDir);
}
}).whenComplete(Schedulers.javafx(), (exception) -> {
if (exception != null) {
Controllers.dialog(i18n("message.failed") + "\n" + StringUtils.getStackTrace(exception), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
}
}).start();
}
private void clearJunkFiles() {
@@ -67,7 +67,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
private final TabHeader header = new TabHeader(transitionPane);
private final TabHeader.Tab<WorldInfoPage> worldInfoTab = new TabHeader.Tab<>("worldInfoPage");
private final TabHeader.Tab<WorldBackupsPage> worldBackupsTab = new TabHeader.Tab<>("worldBackupsPage");
private final TabHeader.Tab<DatapackListPage> datapackTab = new TabHeader.Tab<>("datapackListPage");
private final TabHeader.Tab<DataPackListPage> dataPackTab = new TabHeader.Tab<>("dataPackListPage");
public WorldManagePage(World world, Profile profile, String instanceId) {
this.world = world;
@@ -86,7 +86,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
worldInfoTab.setNodeSupplier(() -> new WorldInfoPage(this));
worldBackupsTab.setNodeSupplier(() -> new WorldBackupsPage(this));
datapackTab.setNodeSupplier(() -> new DatapackListPage(this));
dataPackTab.setNodeSupplier(() -> new DataPackListPage(this));
this.state = new SimpleObjectProperty<>(new State(i18n("world.manage.title", StringUtils.parseColorEscapes(world.getWorldName())), null, true, true, true));
@@ -218,9 +218,9 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
tabBar.addNavigationDrawerTab(getSkinnable().header, getSkinnable().worldInfoTab, i18n("world.info"), SVG.INFO, SVG.INFO_FILL)
.addNavigationDrawerTab(getSkinnable().header, getSkinnable().worldBackupsTab, i18n("world.backup"), SVG.ARCHIVE, SVG.ARCHIVE_FILL);
if (getSkinnable().world.supportDatapacks()) {
getSkinnable().header.getTabs().add(getSkinnable().datapackTab);
tabBar.addNavigationDrawerTab(getSkinnable().header, getSkinnable().datapackTab, i18n("world.datapack"), SVG.EXTENSION, SVG.EXTENSION_FILL);
if (getSkinnable().world.supportDataPacks()) {
getSkinnable().header.getTabs().add(getSkinnable().dataPackTab);
tabBar.addNavigationDrawerTab(getSkinnable().header, getSkinnable().dataPackTab, i18n("world.datapack"), SVG.EXTENSION, SVG.EXTENSION_FILL);
}
}
@@ -67,7 +67,7 @@ public final class UpdateHandler {
performMigration();
} catch (IOException e) {
LOG.warning("Failed to perform migration", e);
SwingUtils.showErrorDialog(i18n("fatal.apply_update_failure", Metadata.PUBLISH_URL) + "\n" + StringUtils.getStackTrace(e));
SwingUtils.showErrorDialog(i18n("fatal.apply_update_failure", Metadata.MANUAL_UPDATE_URL) + "\n" + StringUtils.getStackTrace(e));
}
return true;
}
@@ -82,7 +82,7 @@ public final class UpdateHandler {
applyUpdate(Paths.get(args[1]));
} catch (IOException e) {
LOG.warning("Failed to apply update", e);
SwingUtils.showErrorDialog(i18n("fatal.apply_update_failure", Metadata.PUBLISH_URL) + "\n" + StringUtils.getStackTrace(e));
SwingUtils.showErrorDialog(i18n("fatal.apply_update_failure", Metadata.MANUAL_UPDATE_URL) + "\n" + StringUtils.getStackTrace(e));
}
return true;
}
@@ -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();
@@ -69,6 +69,11 @@
"subtitle" : "Copyright © 2015 Andrew Ellerton.\nLicensed under the Apache 2.0 License.",
"externalLink" : "https://github.com/aellerton/japng"
},
{
"title" : "fxsvgimage",
"subtitle" : "Copyright (c) 2021, 2022, 2025 Hervé Girod.\nLicensed under the BSD 3-clause License.",
"externalLink" : "https://github.com/hervegirod/fxsvgimage"
},
{
"title": "Terracotta",
"subtitle": "Copyright © 2025 Burning_TNT.\nLicensed under the AGPL 3.0 License.",
@@ -1463,6 +1463,11 @@
-jfx-radius: 9;
}
.spinner-pane .spinner-pane-content {
-fx-padding: 10px;
-fx-spacing: 10px;
}
/*******************************************************************************
* *
* JFX Combo Box *
@@ -1565,6 +1570,21 @@
-fx-background-color: -monet-on-surface;
}
.custom-color-dialog .jfx-color-dialog-actions {
-fx-background-color: -monet-surface;
-fx-padding: 0.0 10.0 5.0 0.0;
-fx-alignment: center-right;
}
.custom-color-dialog .jfx-color-dialog-accept {
-fx-padding: 0.7em 0.8em;
}
.custom-color-dialog .jfx-color-dialog-cancel {
-fx-text-fill: -monet-on-surface-variant;
-fx-padding: 0.7em 0.8em;
}
/*******************************************************************************
* *
* Table View *
@@ -1924,3 +1944,7 @@
.line-toggle-button .jfx-toggle-button {
-fx-padding: 0;
}
.update-pane:active .subtitle-label {
-fx-text-fill: -monet-tertiary;
}
@@ -167,6 +167,9 @@ account.skin.upload.failed=Failed to upload skin.
account.skin.invalid_skin=Invalid skin file.
account.username=Username
addon.download.title.release=Minecraft %s
addon.download.title.snapshot=Minecraft %s (Snapshots)
archive.author=Author(s)
archive.date=Publish Date
archive.file.name=File Name
@@ -397,7 +400,7 @@ extension.ps1=Windows PowerShell Script
extension.sh=Shell Script
extension.command=macOS Shell Script
extension.datapack=Datapack Archive
extension.datapack=Data Pack Archive
extension.mod=Mod File
extension.modloader.installer=Mod Loader Installer
extension.resourcepack=Resource Pack Archive
@@ -455,6 +458,8 @@ fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher has provided
\n\
If you are using the <b>Qualcomm</b> platform, you may need to install the <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL Compatibility Pack</a> before playing games.\n\
Click the link to navigate to the Microsoft Store and install the compatibility pack.
# When translating the following string into non-Latin languages, please retain the English translation
fatal.wine_warning=HMCL is running under Wine or one of its distributions.\nAs a cross-platform application, HMCL is best experienced natively on your host OS.\nPress "OK" to continue, but please note that some issues may arise.
file=File
@@ -702,7 +707,7 @@ game.version=Game Instance
help=Help
help.doc=Hello Minecraft! Launcher Documentation
help.detail=For datapack and modpack makers.
help.detail=For data pack and modpack makers.
input.email=The username must be an email address.
input.number=The input must be numbers.
@@ -808,6 +813,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.
@@ -990,7 +997,7 @@ modrinth.category.colored-lighting=Colored Lighting
modrinth.category.combat=Combat
modrinth.category.core-shaders=Core Shaders
modrinth.category.cursed=Cursed
modrinth.category.datapack=Datapack
modrinth.category.datapack=Data Pack
modrinth.category.decoration=Decoration
modrinth.category.economy=Economy
modrinth.category.entities=Entities
@@ -1133,11 +1140,11 @@ nbt.open.failed=Failed to open file
nbt.save.failed=Failed to save file
nbt.title=View File - %s
datapack=Datapacks
datapack=Data Packs
datapack.add=Add
datapack.add.title=Choose datapack archive you want to add
datapack.reload.toast=Minecraft is running, please use the /reload command to reload the data pack
datapack.title=World [%s] - Datapacks
datapack.add.title=Choose data pack archive you want to add
datapack.reload.toast=Minecraft is running. Use the /reload command to reload data packs
datapack.title=World [%s] - Data Packs
web.failed=Failed to load page
web.open_in_browser=Do you want to open this address in a browser:\n%s
@@ -1167,7 +1174,7 @@ world.duplicate.failed.empty_name=Name cannot be empty
world.duplicate.failed.invalid_name=Name contains invalid characters
world.duplicate.failed=Failed to duplicate the world
world.duplicate.success.toast=Successfully duplicated the world
world.datapack=Datapacks
world.datapack=Data Packs
world.datetime=Last played on %s
world.delete=Delete the World
world.delete.failed=Failed to delete world.\n%s
@@ -1417,6 +1424,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
@@ -1432,6 +1441,7 @@ settings.launcher.download.threads.auto=Automatically Determine
settings.launcher.download.threads.hint=Too many threads may cause your system to freeze, and your download speed may be affected by your ISP and download servers. It is not always the case that more threads increase your download speed.
settings.launcher.download_source=Download Source
settings.launcher.download_source.auto=Automatically Choose Download Sources
settings.launcher.default_addon_source=Default Addon Source
settings.launcher.enable_game_list=Show instance list in homepage
settings.launcher.font=Font
settings.launcher.font.anti_aliasing=Anti-aliasing
@@ -1580,7 +1590,7 @@ update=Update
update.accept=Update
update.changelog=Changelog
update.channel.dev=Beta
update.channel.dev.hint=You are currently using a Beta channel build of the launcher. While it may include some extra features, it is also sometimes less stable than the Release channel builds.\n\
update.channel.dev.hint=You are currently using a Beta channel build of the launcher. While it may include some extra features, it is also sometimes less stable than the Stable channel builds.\n\
\n\
If you encounter any bugs or problems, please submit feedback via the channels provided on the <a href="hmcl://settings/feedback">Feedback</a> page.\n\
\n\
@@ -1593,7 +1603,7 @@ update.channel.nightly.hint=You are currently using a Nightly channel build of t
\n\
Follow <a href="https://space.bilibili.com/1445341">@huanghongxun</a> on Bilibili to stay up to date on important HMCL news, or <a href="https://space.bilibili.com/20314891">@Glavo</a> to learn about HMCL development progress.
update.channel.nightly.title=Nightly Channel Notice
update.channel.stable=Release
update.channel.stable=Stable
update.checking=Checking for updates
update.disable_auto_show_update_dialog=Do not show update dialog automatically
update.disable_auto_show_update_dialog.subtitle=Enable this option to prevent HMCL from automatically showing the update dialog.
@@ -1602,7 +1612,8 @@ update.found=Update Available!
update.newest_version=Latest version: %s
update.bubble.title=Update Available: %s
update.bubble.subtitle=Click here to update
update.note=Beta and Nightly channels may have more features or fixes, but they also come with more potential problems.
update.note.stable=Suitable for users who prioritize software stability.\nNew features are added to the stable version only after thorough testing.
update.note.dev=Suitable for users who want to experience new features first.\nThe development version includes the latest features and bug fixes,\nbut may also have more issues due to insufficient testing.
update.latest=This is the latest version
update.no_browser=Cannot open in system browser. But we copied the link to your clipboard, and you can open it manually.
update.tooltip=Update
@@ -1593,7 +1593,6 @@ update.found=تحديث متاح!
update.newest_version=أحدث إصدار: %s
update.bubble.title=تحديث متاح: %s
update.bubble.subtitle=انقر هنا للتحديث
update.note=قد تحتوي القنوات التجريبية والليلية على مزيد من الميزات والإصلاحات، لكنها تأتي أيضاً بمزيد من المشاكل المحتملة.
update.latest=هذا هو أحدث إصدار
update.no_browser=لا يمكن الفتح في متصفح النظام. لكننا نسخنا الرابط إلى حافظتك، ويمكنك فتحه يدوياً.
update.tooltip=تحديث
@@ -1402,7 +1402,6 @@ update.found=¡Actualización disponible!
update.newest_version=Última versión: %s
update.bubble.title=Actualización disponible: %s
update.bubble.subtitle=Haga clic aquí para actualizar
update.note=Advertencia: Las versiones beta y las versiones nocturnas pueden tener más funciones o correcciones, pero también tienen más problemas potenciales.
update.latest=Esta es la última versión.
update.no_browser=No se puede abrir en el navegador del sistema. Pero, hemos copiado el enlace a su portapapeles y puede abrirlo manualmente.
update.tooltip=Actualización
@@ -912,7 +912,6 @@ update.found=アップデートが利用可能です!
update.newest_version=最新バージョン:%s
update.bubble.title=利用可能な更新:%s
update.bubble.subtitle=更新するにはここをクリックしてください
update.note=警告:ベータ版には、より多くの機能とバグ修正が含まれている可能性がありますが、エラーが発生する可能性もあります。
update.latest=これは最新バージョンです。
update.no_browser=ブラウザを開くことができません。リンクがクリップボードにコピーされました。ブラウザのアドレスバーに貼り付けて更新します。
update.tooltip=更新
@@ -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=覽於理案臺
@@ -1124,6 +1129,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 +1201,8 @@ sponsor.hmcl=Hello Minecraft! Launcher,乃一免費,自由,開源之礦藝
system.architecture=大構
system.operating_system=械網
terracotta=眾戲
unofficial.hint=子方用非官築之 HMCL。余無能證其安,請君自辨。
update=啟者迭更
@@ -1211,15 +1220,18 @@ 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
update.bubble.title=尋得更新:%s
update.bubble.subtitle=是擊以晉
update.note=开發版與預版,虽增功、修阙,然亦恐生他患。
update.latest=當版至新
update.no_browser=無啟瀏覽器。址固鈔訖,君可自粘之以啟頁。
update.tooltip=迭更
update.preview=先覽 HMCL 之版
update.preview.subtitle=啟此,君可先得 HMCL 之新,試之於未行。
version=戲
version.name=戲例之名
@@ -1392,7 +1392,6 @@ update.found=Доступно обновление!
update.newest_version=Последняя версия: %s
update.bubble.title=Доступно обновление: %s
update.bubble.subtitle=Кликните здесь для обновления
update.note=Предупреждение: Бета-версии могут иметь больше функциональных возможностей и исправлений ошибок, но также и больше возможных ошибок.
update.latest=Это последняя версия.
update.no_browser=Не удалось открыть браузер. Ссылка была скопирована в буфер обмена. Вставьте его в адресную строку браузера для обновления.
update.tooltip=Обновить
@@ -1343,7 +1343,6 @@ update.found=Доступне оновлення!
update.newest_version=Остання версія: %s
update.bubble.title=Доступне оновлення: %s
update.bubble.subtitle=Натисніть тут, щоб оновити
update.note=Канали Бета та Нічний можуть мати більше функцій або виправлень, але вони також мають більше потенційних проблем.
update.latest=Це остання версія
update.no_browser=Не вдається відкрити в системному браузері. Але ми скопіювали посилання до вашого буфера обміну, і ви можете відкрити його вручну.
update.tooltip=Оновити
@@ -164,6 +164,9 @@ account.skin.upload.failed=外觀上傳失敗
account.skin.invalid_skin=無法識別的外觀檔案
account.username=使用者名稱
addon.download.title.release=Minecraft %s
addon.download.title.snapshot=Minecraft %s (快照)
archive.author=作者
archive.date=發布日期
archive.file.name=檔案名稱
@@ -418,6 +421,8 @@ fatal.unsupported_platform=Minecraft 尚未對您的平臺提供完善支援,
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher 已為龍芯提供支援。\n如果遇到問題,你可以點擊右上角幫助按鈕進行求助。
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Launcher 已為 Apple Silicon 平臺提供支援。使用 ARM 原生 Java 啟動遊戲以獲得更流暢的遊戲體驗。\n如果你在遊戲中遭遇問題,使用 x86-64 架構的 Java 啟動遊戲可能有更好的相容性。
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher 已為 Windows on Arm 平臺提供原生支援。如果你在遊戲中遭遇問題,請嘗試使用 x86 架構的 Java 啟動遊戲。\n\n如果你正在使用<b>高通</b>平臺,你可能需要安裝 <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL 相容包</a>後才能進行遊戲。點擊連結前往 Microsoft Store 安裝相容包。
# When translating the following string into non-Latin languages, please retain the English translation
fatal.wine_warning=HMCL is running under Wine or one of its distributions.\nAs a cross-platform application, HMCL is best experienced natively on your host OS.\nPress "OK" to continue, but please note that some issues may arise.\n\n您目前正在使用 Wine 或其衍生版本執行 HMCL。\nHMCL 是跨平臺的,我們建議您直接在主機系統上執行。\n按下「確定」以繼續,但請注意,此操作可能會引發一些問題。
file=檔案
@@ -612,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 崩潰。建議你到 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 後再次啟動。
@@ -1202,6 +1209,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=跟隨系統設定
@@ -1217,6 +1226,7 @@ settings.launcher.download.threads.auto=自動選取執行緒數
settings.launcher.download.threads.hint=執行緒數過高可能導致系統卡頓。你的下載速度會受到網際網路運營商、下載來源伺服器等方面的影響。調高下載執行緒數不一定能大幅提升總下載速度。
settings.launcher.download_source=下載來源
settings.launcher.download_source.auto=自動選取下載來源
settings.launcher.default_addon_source=遊戲內容預設下載來源
settings.launcher.enable_game_list=在首頁內顯示遊戲清單
settings.launcher.font=字體
settings.launcher.font.anti_aliasing=反鋸齒
@@ -1383,7 +1393,8 @@ update.found=發現到更新
update.newest_version=最新版本為:%s
update.bubble.title=發現更新:%s
update.bubble.subtitle=點選此處進行升級
update.note=開發版與預覽版包含更多的功能以及錯誤修復,但也可能會包含其他的問題。
update.note.stable=適合優先追求軟體穩定性的使用者使用。\n新功能在經過充分測試後才會被添加到穩定版中。
update.note.dev=適合希望優先體驗新功能的使用者使用。\n開發版會包含最新功能和錯誤修復,但未經充分測試,可能會存在更多問題。
update.latest=目前版本為最新版本
update.no_browser=無法開啟瀏覽器。網址已經複製到剪貼簿了,你可以手動複製網址開啟頁面。
update.tooltip=更新
@@ -166,6 +166,9 @@ account.skin.upload.failed=皮肤上传失败
account.skin.invalid_skin=无法识别的皮肤文件
account.username=用户名
addon.download.title.release=Minecraft %s
addon.download.title.snapshot=Minecraft %s (快照)
archive.author=作者
archive.date=发布日期
archive.file.name=文件名
@@ -422,6 +425,8 @@ fatal.unsupported_platform=Minecraft 尚未对你的平台提供完善支持,
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher 已为龙芯提供支持。\n如果遇到问题,你可以点击右上角帮助按钮进行求助。
fatal.unsupported_platform.macos_arm64=Hello Minecraft! Launcher 已为 Apple Silicon 平台提供支持。使用 ARM 原生 Java 启动游戏以获得更流畅的游戏体验。\n如果你在游戏中遇到问题,使用 x86-64 架构的 Java 启动游戏可能有更好的兼容性。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher 已为 Windows on Arm 平台提供原生支持。如果你在游戏中遇到问题,请尝试使用 x86 架构的 Java 启动游戏。\n如果你正在使用 <b>高通</b> 平台,你可能需要安装 <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL 兼容包</a> 后才能进行游戏。点击链接前往 Microsoft Store 安装兼容包。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
# When translating the following string into non-Latin languages, please retain the English translation
fatal.wine_warning=HMCL is running under Wine or one of its distributions.\nAs a cross-platform application, HMCL is best experienced natively on your host OS.\nPress "OK" to continue, but please note that some issues may arise.\n\n您目前正在使用 Wine 或其发行版运行 HMCL。\nHMCL 是跨平台的,我们建议您直接在宿主系统上运行。\n按下“确定”以继续,但请注意,此操作可能会引发一些问题。
file=文件
@@ -617,6 +622,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 后再次启动。
@@ -1207,6 +1214,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=跟随系统设置
@@ -1222,6 +1231,7 @@ settings.launcher.download.threads.auto=自动选择线程数
settings.launcher.download.threads.hint=线程数过高可能导致系统卡顿。你的下载速度会受到互联网运营商、下载源服务器等方面的影响。调高下载线程数不一定能大幅提升总下载速度。
settings.launcher.download_source=下载源
settings.launcher.download_source.auto=自动选择下载源
settings.launcher.default_addon_source=游戏内容默认下载源
settings.launcher.enable_game_list=在主页内显示版本列表
settings.launcher.font=字体
settings.launcher.font.anti_aliasing=抗锯齿
@@ -1388,7 +1398,8 @@ update.found=发现更新
update.newest_version=最新版本为:%s
update.bubble.title=发现更新:%s
update.bubble.subtitle=点击此处进行升级
update.note=开发版与预览版包含更多的功能以及错误修复,但也可能会包含其他的问题。
update.note.stable=适合优先追求软件稳定性的用户使用。\n新功能在经过充分测试后才会被添加到稳定版中。
update.note.dev=适合希望优先体验新功能的用户使用。\n包含最新功能和错误修复,但未经充分测试,可能会存在更多问题。
update.latest=当前版本为最新版本
update.no_browser=无法打开浏览器。网址已经复制到剪贴板,你可以手动粘贴网址打开页面。
update.tooltip=更新
@@ -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)
@@ -222,7 +222,7 @@ public final class World {
return isLocked(getSessionLockFile());
}
public boolean supportDatapacks() {
public boolean supportDataPacks() {
return getGameVersion() != null && getGameVersion().isAtLeast("1.13", "17w43a");
}
@@ -39,14 +39,14 @@ import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class Datapack {
public class DataPack {
private static final String DISABLED_EXT = "disabled";
private static final String ZIP_EXT = "zip";
private final Path path;
private final ObservableList<Pack> packs = FXCollections.observableArrayList();
public Datapack(Path path) {
public DataPack(Path path) {
this.path = path;
}
@@ -58,14 +58,14 @@ public class Datapack {
return packs;
}
public static void installPack(Path sourceDatapackPath, Path targetDatapackDirectory, GameVersionNumber gameVersionNumber) throws IOException {
public static void installPack(Path sourceDataPackPath, Path targetDataPackDirectory, GameVersionNumber gameVersionNumber) throws IOException {
boolean containsMultiplePacks;
Set<String> packs = new HashSet<>();
try (FileSystem fs = CompressingUtils.readonly(sourceDatapackPath).setAutoDetectEncoding(true).build()) {
Path datapacks = fs.getPath("datapacks");
try (FileSystem fs = CompressingUtils.readonly(sourceDataPackPath).setAutoDetectEncoding(true).build()) {
Path dataPacks = fs.getPath("datapacks");
Path mcmeta = fs.getPath("pack.mcmeta");
if (Files.exists(datapacks)) {
if (Files.exists(dataPacks)) {
containsMultiplePacks = true;
} else if (Files.exists(mcmeta)) {
containsMultiplePacks = false;
@@ -74,14 +74,14 @@ public class Datapack {
}
if (containsMultiplePacks) {
try (Stream<Path> s = Files.list(datapacks)) {
try (Stream<Path> s = Files.list(dataPacks)) {
packs = s.map(FileUtils::getNameWithoutExtension).collect(Collectors.toSet());
}
} else {
packs.add(FileUtils.getNameWithoutExtension(sourceDatapackPath));
packs.add(FileUtils.getNameWithoutExtension(sourceDataPackPath));
}
try (DirectoryStream<Path> stream = Files.newDirectoryStream(targetDatapackDirectory)) {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(targetDataPackDirectory)) {
for (Path dir : stream) {
String packName = FileUtils.getName(dir);
if (FileUtils.getExtension(dir).equals(DISABLED_EXT)) {
@@ -100,9 +100,9 @@ public class Datapack {
}
if (!containsMultiplePacks) {
FileUtils.copyFile(sourceDatapackPath, targetDatapackDirectory.resolve(FileUtils.getName(sourceDatapackPath)));
FileUtils.copyFile(sourceDataPackPath, targetDataPackDirectory.resolve(FileUtils.getName(sourceDataPackPath)));
} else {
new Unzipper(sourceDatapackPath, targetDatapackDirectory)
new Unzipper(sourceDataPackPath, targetDataPackDirectory)
.setReplaceExistentFile(true)
.setSubDirectory("/datapacks/")
.unzip();
@@ -113,14 +113,14 @@ public class Datapack {
&& gameVersionNumber.compareTo("26.1-snapshot-6") >= 0;
if (useNewResourcePath) {
Files.createDirectories(targetDatapackDirectory.getParent().resolve("resourcepacks"));
targetResourceZipPath = targetDatapackDirectory.getParent().resolve("resourcepacks/resources.zip");
Files.createDirectories(targetDataPackDirectory.getParent().resolve("resourcepacks"));
targetResourceZipPath = targetDataPackDirectory.getParent().resolve("resourcepacks/resources.zip");
} else {
targetResourceZipPath = targetDatapackDirectory.getParent().resolve("resources.zip");
targetResourceZipPath = targetDataPackDirectory.getParent().resolve("resources.zip");
}
try (FileSystem outputResourcesZipFS = CompressingUtils.createWritableZipFileSystem(targetResourceZipPath);
FileSystem inputPackZipFS = CompressingUtils.createReadOnlyZipFileSystem(sourceDatapackPath)) {
FileSystem inputPackZipFS = CompressingUtils.createReadOnlyZipFileSystem(sourceDataPackPath)) {
Path resourcesZip = inputPackZipFS.getPath("resources.zip");
if (Files.isRegularFile(resourcesZip)) {
Path tempResourcesFile = Files.createTempFile("hmcl", ".zip");
@@ -229,14 +229,14 @@ public class Datapack {
}
}
private Optional<Pack> parsePack(Path datapackPath, boolean isDirectory, String name, Path mcmetaPath) {
private Optional<Pack> parsePack(Path dataPackPath, boolean isDirectory, String name, Path mcmetaPath) {
try {
PackMcMeta mcMeta = JsonUtils.fromNonNullJson(Files.readString(mcmetaPath), PackMcMeta.class);
return Optional.of(new Pack(datapackPath, isDirectory, name, mcMeta.pack().description(), this));
return Optional.of(new Pack(dataPackPath, isDirectory, name, mcMeta.pack().description(), this));
} catch (JsonParseException e) {
LOG.warning("Invalid pack.mcmeta format in " + datapackPath, e);
LOG.warning("Invalid pack.mcmeta format in " + dataPackPath, e);
} catch (IOException e) {
LOG.warning("IO error reading " + datapackPath, e);
LOG.warning("IO error reading " + dataPackPath, e);
}
return Optional.empty();
}
@@ -248,14 +248,14 @@ public class Datapack {
private final BooleanProperty activeProperty;
private final String id;
private final LocalModFile.Description description;
private final Datapack parentDatapack;
private final DataPack parentDataPack;
public Pack(Path path, boolean isDirectory, String id, LocalModFile.Description description, Datapack parentDatapack) {
public Pack(Path path, boolean isDirectory, String id, LocalModFile.Description description, DataPack parentDataPack) {
this.path = path;
this.isDirectory = isDirectory;
this.id = id;
this.description = description;
this.parentDatapack = parentDatapack;
this.parentDataPack = parentDataPack;
this.statusFile = initializeStatusFile(path, isDirectory);
this.activeProperty = initializeActiveProperty();
@@ -313,8 +313,8 @@ public class Datapack {
return description;
}
public Datapack getParentDatapack() {
return parentDatapack;
public DataPack getParentDataPack() {
return parentDataPack;
}
public BooleanProperty activeProperty() {
@@ -86,4 +86,9 @@ public final class SettingsMap {
public void clear() {
map.clear();
}
@Override
public String toString() {
return "SettingsMap" + map;
}
}
@@ -282,6 +282,25 @@ public enum OperatingSystem {
return SYSTEM_VERSION.isAtLeast(OSVersion.WINDOWS_7);
}
public static boolean isRunningUnderWine() {
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS) {
return false;
}
String[] wineEnvVars = {
"WINEPREFIX",
"WINELOADER",
"WINEDLLPATH"
};
for (String var : wineEnvVars) {
if (System.getenv(var) != null) {
return true;
}
}
return false;
}
public static Path getWorkingDirectory(String folder) {
String home = System.getProperty("user.home", ".");
switch (OperatingSystem.CURRENT_OS) {
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.util.versioning;
import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.IOException;
@@ -189,6 +190,32 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
return buildDebugString().toString();
}
public static @Nullable GameVersionNumber getReleaseOfSnapshot(GameVersionNumber gameVersion) {
if (gameVersion instanceof Release release) {
if (release.getEaType() == Release.ReleaseType.GA) {
return null;
}
if (release.getPatch() > 0) {
return asGameVersion(release.getMajor() + "." + release.getMinor() + "." + release.getPatch());
} else {
return asGameVersion(release.getMajor() + "." + release.getMinor());
}
}
if (gameVersion instanceof LegacySnapshot snapshot) {
String[] defaultVersions = Versions.DEFAULT_GAME_VERSIONS;
for (int i = defaultVersions.length - 1; i >= 0; i--) {
Release gaRelease = (Release) asGameVersion(defaultVersions[i]);
if (gaRelease.compareToSnapshot(snapshot) > 0) {
return gaRelease;
}
}
}
return null;
}
public static final class Old extends GameVersionNumber {
static Old parse(String value) {
if (value.isEmpty())
@@ -627,6 +654,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);
@@ -912,3 +912,6 @@
26.1-rc-1
26.1-rc-2
26.1
26.1.1-rc-1
26.1.1
26w14a
@@ -1,4 +1,4 @@
# https://gitlab.freedesktop.org/mesa/libdrm/-/blob/35a21916c8f621b35a67f3c5994280ae59a4981a/data/amdgpu.ids
# https://gitlab.freedesktop.org/mesa/libdrm/-/blob/15cdcfe9e1c4b00f2a819bc1f4b3efa1326ac6bc/data/amdgpu.ids
#
# List of AMDGPU IDs
#
@@ -10,6 +10,19 @@
1114, C3, AMD Radeon 840M Graphics
1114, D2, AMD Radeon 860M Graphics
1114, D3, AMD Radeon 840M Graphics
1114, E2, AMD Radeon 860M Graphics
1114, E4, AMD Radeon 860M Graphics
1114, E5, AMD Radeon 840M Graphics
1114, E9, AMD Radeon 860M Graphics
1114, EA, AMD Radeon 840M Graphics
1114, ED, AMD Radeon 860M Graphics
1114, EE, AMD Radeon 840M Graphics
1114, F2, AMD Radeon 860M Graphics
1114, F3, AMD Radeon 840M Graphics
1114, F9, AMD Radeon 860M Graphics
1114, FA, AMD Radeon 840M Graphics
1114, FC, AMD Radeon 860M Graphics
1114, FD, AMD Radeon 840M Graphics
1309, 00, AMD Radeon R7 Graphics
130A, 00, AMD Radeon R6 Graphics
130B, 00, AMD Radeon R4 Graphics
@@ -41,23 +54,35 @@
1506, C3, AMD Radeon 610M
1506, C4, AMD Radeon 610M
150E, C1, AMD Radeon 890M Graphics
150E, C4, AMD Radeon 890M Graphics
150E, C4, AMD Radeon 880M Graphics
150E, C5, AMD Radeon 890M Graphics
150E, C6, AMD Radeon 890M Graphics
150E, C7, AMD Radeon 890M Graphics
150E, D1, AMD Radeon 890M Graphics
150E, D2, AMD Radeon 890M Graphics
150E, D2, AMD Radeon 880M Graphics
150E, D3, AMD Radeon 890M Graphics
1586, C1, Radeon 8060S Graphics
1586, C2, Radeon 8050S Graphics
1586, C4, Radeon 8050S Graphics
1586, D1, Radeon 8060S Graphics
1586, D2, Radeon 8050S Graphics
1586, D4, Radeon 8050S Graphics
1586, D5, Radeon 8040S Graphics
150E, E1, AMD Radeon 890M Graphics
150E, E3, AMD Radeon 890M Graphics
150E, E4, AMD Radeon 890M Graphics
150E, F1, AMD Radeon 890M Graphics
150E, F3, AMD Radeon 890M Graphics
1586, C1, AMD Radeon 8060S Graphics
1586, C2, AMD Radeon 8050S Graphics
1586, C3, AMD Radeon 8060S Graphics
1586, C4, AMD Radeon 8050S Graphics
1586, C6, AMD Radeon 8060S Graphics
1586, D1, AMD Radeon 8060S Graphics
1586, D2, AMD Radeon 8050S Graphics
1586, D4, AMD Radeon 8050S Graphics
1586, D5, AMD Radeon 8040S Graphics
15BF, 00, AMD Radeon 780M Graphics
15BF, 01, AMD Radeon 760M Graphics
15BF, 02, AMD Radeon 780M Graphics
15BF, 03, AMD Radeon 760M Graphics
15BF, 05, AMD Radeon 760M Graphics
15BF, 06, AMD Radeon 780M Graphics
15BF, 07, AMD Radeon 740M Graphics
15BF, 08, AMD Radeon 740M Graphics
15BF, C1, AMD Radeon 780M Graphics
15BF, C2, AMD Radeon 780M Graphics
15BF, C3, AMD Radeon 760M Graphics
@@ -71,6 +96,7 @@
15BF, CB, AMD Radeon 760M Graphics
15BF, CC, AMD Radeon 740M Graphics
15BF, CD, AMD Radeon 760M Graphics
15BF, CE, AMD Radeon 740M Graphics
15BF, CF, AMD Radeon 780M Graphics
15BF, D0, AMD Radeon 780M Graphics
15BF, D1, AMD Radeon 780M Graphics
@@ -93,10 +119,18 @@
15C8, C2, AMD Radeon 740M Graphics
15C8, C3, AMD Radeon 740M Graphics
15C8, C4, AMD Radeon 740M Graphics
15C8, C5, AMD Radeon 740M Graphics
15C8, C6, AMD Radeon 740M Graphics
15C8, C7, AMD Radeon 740M Graphics
15C8, C8, AMD Radeon 740M Graphics
15C8, D1, AMD Radeon 740M Graphics
15C8, D2, AMD Radeon 740M Graphics
15C8, D3, AMD Radeon 740M Graphics
15C8, D4, AMD Radeon 740M Graphics
15C8, D5, AMD Radeon 740M Graphics
15C8, D6, AMD Radeon 740M Graphics
15C8, D7, AMD Radeon 740M Graphics
15C8, D8, AMD Radeon 740M Graphics
15D8, 00, AMD Radeon RX Vega 8 Graphics WS
15D8, 91, AMD Radeon Vega 3 Graphics
15D8, 91, AMD Ryzen Embedded R1606G with Radeon Vega Gfx
@@ -252,6 +286,19 @@
1901, D6, AMD Radeon 740M Graphics
1901, D7, AMD Radeon 740M Graphics
1901, D8, AMD Radeon 740M Graphics
1902, C0, AMD Radeon 840M Graphics
1902, C1, AMD Radeon 840M Graphics
1902, C2, AMD Radeon 820M Graphics
1902, C3, AMD Radeon 840M Graphics
1902, C6, AMD Radeon 820M Graphics
1902, C7, AMD Radeon 840M Graphics
1902, C8, AMD Radeon 840M Graphics
1902, C9, AMD Radeon 820M Graphics
1902, CA, AMD Radeon 840M Graphics
1902, D1, AMD Radeon 840M Graphics
1902, D3, AMD Radeon 840M Graphics
1902, D7, AMD Radeon 840M Graphics
1902, D8, AMD Radeon 840M Graphics
6600, 00, AMD Radeon HD 8600 / 8700M
6600, 81, AMD Radeon R7 M370
6601, 00, AMD Radeon HD 8500M / 8700M
@@ -579,6 +626,7 @@
7480, C3, AMD Radeon RX 7600S
7480, C7, AMD Radeon RX 7600M XT
7480, CF, AMD Radeon RX 7600
7481, C7, AMD Steam Machine
7483, CF, AMD Radeon RX 7600M
7489, 00, AMD Radeon Pro W7500
7499, 00, AMD Radeon Pro W7400
@@ -597,12 +645,14 @@
7550, C2, AMD Radeon RX 9070 GRE
7550, C3, AMD Radeon RX 9070
7551, C0, AMD Radeon AI PRO R9700
7551, C8, AMD Radeon AI PRO R9600D
7590, C0, AMD Radeon RX 9060 XT
7590, C1, AMD Radeon RX 9060 XT LP
7590, C7, AMD Radeon RX 9060
75A0, C0, AMD Instinct MI350X
75A3, C0, AMD Instinct MI355X
75B0, C0, AMD Instinct MI350X VF
75B3, C0, AMD Instinct MI355X VF
75A0, 00, AMD Instinct MI350X
75A3, 00, AMD Instinct MI355X
75B0, 00, AMD Instinct MI350X VF
75B3, 00, AMD Instinct MI355X VF
9830, 00, AMD Radeon HD 8400 / R3 Series
9831, 00, AMD Radeon HD 8400E
9832, 00, AMD Radeon HD 8330
@@ -28,6 +28,7 @@ import java.util.*;
import java.util.function.Supplier;
import static org.jackhuang.hmcl.util.versioning.GameVersionNumber.asGameVersion;
import static org.jackhuang.hmcl.util.versioning.GameVersionNumber.getReleaseOfSnapshot;
import static org.junit.jupiter.api.Assertions.*;
/**
@@ -376,10 +377,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",
@@ -507,4 +509,14 @@ public final class GameVersionNumberTest {
assertThrows(IllegalArgumentException.class, () -> asGameVersion("17w43a").isAtLeast("1.13", "22w13oneblockatatime", true));
assertThrows(IllegalArgumentException.class, () -> asGameVersion("17w43a").isAtLeast("1.13", "22w13oneblockatatime", false));
}
@Test
public void testGetReleaseOfSnapshot() {
assertEquals(asGameVersion("1.21.11"), getReleaseOfSnapshot(asGameVersion("25w45a")));
assertEquals(asGameVersion("1.21.11"), getReleaseOfSnapshot(asGameVersion("25w45a_unobfuscated")));
assertEquals(asGameVersion("1.21.11"), getReleaseOfSnapshot(asGameVersion("1.21.11-pre3")));
assertEquals(asGameVersion("26.1"), getReleaseOfSnapshot(asGameVersion("26.1-snapshot-9")));
assertNull(getReleaseOfSnapshot(asGameVersion("26.1")));
assertNull(getReleaseOfSnapshot(asGameVersion("20w14infinite")));
}
}
+13 -13
View File
@@ -17,7 +17,7 @@ while the minor version is incremented regularly according to the release schedu
### Version Types
HMCL has two types of versions: **Stable** and **Development**.
HMCL has two types of versions: **Stable** and **Beta**.
#### Stable Version
@@ -27,19 +27,19 @@ New features are merged into the stable version only after thorough testing.
The stable version number follows the naming rule `<version branch>.<build number>` (e.g., `3.7.1`).
The build number is calculated independently for each version branch.
#### Development Version
#### Beta Version
The HMCL development version is suitable for users who want to experience new features first.
The development version includes the latest features and bug fixes,
The HMCL beta version is suitable for users who want to experience new features first.
The beta version includes the latest features and bug fixes,
but may also have more issues due to insufficient testing.
The development version follows the naming rule `<version branch>.0.<build number>` (e.g., `3.7.0.300`).
The beta version follows the naming rule `<version branch>.0.<build number>` (e.g., `3.7.0.300`).
The build number is shared globally across all version branches.
## Release Channels
HMCL has two main release channels: **Stable Channel** and **Development Channel**.
They are used to release the HMCL stable and development versions, respectively.
HMCL has two main release channels: **Stable Channel** and **Beta Channel**.
They are used to release the HMCL stable and beta versions, respectively.
To test HMCL versions before official release, we will push updates to some users in advance.
Users can enable the "Preview HMCL releases early" option on the "Settings > General" page to receive preview updates from the corresponding channel.
@@ -50,17 +50,17 @@ Users can enable the "Preview HMCL releases early" option on the "Settings > Gen
gantt
title HMCL Version Lifecycle (Example)
section 3.9
Development Phase: a1, 2025-11-15, 30d
Beta Phase: a1, 2025-11-15, 30d
Preview Phase: a2, after a1, 16d
Stable Release: milestone, after a2, 0d
Maintenance Phase: a3, after a2, 31d
section 3.10
Development Phase: b1, after a1, 31d
Beta Phase: b1, after a1, 31d
Preview Phase: b2, after b1, 16d
Stable Release: milestone, after b2, 0d
Maintenance Phase: b3, after b2, 31d
section 3.11
Development Phase: c1, after b1, 31d
Beta Phase: c1, after b1, 31d
Preview Phase: c2, after c1, 16d
Stable Release: milestone, after c2, 0d
Maintenance Phase: c3, after c2, 30d
@@ -71,11 +71,11 @@ and each version branch `x.y` has a lifecycle of two and a half months.
The lifecycle of each version branch is divided into the following stages:
1. **Development Phase** (Mid-month M ~ Mid-month M+1)
1. **Beta Phase** (Mid-month M ~ Mid-month M+1)
During this phase, the version is developed in the Git branch `main`.
Typically, we release a development version `x.y.0.<build number>` based on this branch every week,
Typically, we release a beta version `x.y.0.<build number>` based on this branch every week,
which includes all features and bug fixes merged during the week.
2. **Preview Phase** (Mid-month M+1 ~ End of month M+1)
@@ -84,7 +84,7 @@ The lifecycle of each version branch is divided into the following stages:
which corresponds to the fixed version branch `x.y`.
At the same time, the version branch corresponding to the `main` branch is incremented to `x.y♯`,
entering the development phase of the next version branch.
entering the beta phase of the next version branch.
After the fork, no new features will be added to this version branch;
only bug fixes and data updates will be made.
+8 -4
View File
@@ -1,23 +1,25 @@
[versions]
hmclauncher = "3.7.0.1"
jetbrains-annotations = "26.1.0"
kala-compress = "1.27.1-1"
kala-compress = "1.27.1-3"
simple-png-javafx = "0.3.0"
gson = "2.13.2"
toml4j = "0.7.2"
xz = "1.11"
xz = "1.12"
lz4 = "1.10.4.1"
fx-gson = "5.0.0"
constant-pool-scanner = "1.2"
hello-nbt = "0.2.0"
hello-nbt = "0.3.0"
nanohttpd = "2.3.1"
jsoup = "1.21.2"
chardet = "2.5.0"
twelvemonkeys = "3.13.0"
twelvemonkeys = "3.13.1"
fxsvgimage = "1.3"
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"
@@ -46,11 +48,13 @@ nanohttpd = { module = "org.nanohttpd:nanohttpd", version.ref = "nanohttpd" }
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
chardet = { module = "org.glavo:chardet", version.ref = "chardet" }
twelvemonkeys-imageio-webp = { module = "com.twelvemonkeys.imageio:imageio-webp", version.ref = "twelvemonkeys" }
fxsvgimage = { module = "com.github.hervegirod:fxsvgimage", version.ref = "fxsvgimage" }
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna" }
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" }