From a02e44b41db611857460047700c598c3d4330b72 Mon Sep 17 00:00:00 2001 From: mine_ Date: Tue, 18 Nov 2025 22:21:05 +0800 Subject: [PATCH 01/66] =?UTF-8?q?feat:=E4=BC=98=E5=8C=96=E4=B8=96=E7=95=8C?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E7=95=8C=E9=9D=A2=E5=92=8C=E4=B8=96=E7=95=8C?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/WorldInfoPage.java | 113 +++++++++++------- .../hmcl/ui/versions/WorldListItem.java | 37 +----- .../hmcl/ui/versions/WorldManagePage.java | 45 +++++-- .../hmcl/ui/versions/WorldManageUIUtils.java | 79 ++++++++++++ .../java/org/jackhuang/hmcl/game/World.java | 4 + 5 files changed, 189 insertions(+), 89 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index 34d109304c..f63e6cf54b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -47,9 +47,9 @@ import java.util.Arrays; import java.util.Locale; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; import static org.jackhuang.hmcl.util.i18n.I18n.formatDateTime; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; /** * @author Glavo @@ -108,11 +108,30 @@ private void updateControls() { worldNamePane.setLeft(label); BorderPane.setAlignment(label, Pos.CENTER_LEFT); - Label worldNameLabel = new Label(); - FXUtils.copyOnDoubleClick(worldNameLabel); - worldNameLabel.setText(world.getWorldName()); - BorderPane.setAlignment(worldNameLabel, Pos.CENTER_RIGHT); - worldNamePane.setRight(worldNameLabel); + JFXTextField worldNameField = new JFXTextField(); + worldNameField.setDisable(worldManagePage.isDisable()); + worldNameField.setPrefWidth(200); + BorderPane.setAlignment(worldNameField, Pos.CENTER_RIGHT); + worldNamePane.setRight(worldNameField); + + Tag tag = dataTag.get("LevelName"); + if (tag instanceof StringTag stringTag) { + worldNameField.setText(stringTag.getValue()); + + worldNameField.textProperty().addListener((observable, oldValue, newValue) -> { + if (newValue != null) { + try { + stringTag.setValue(newValue); + world.setWorldName(newValue); + saveLevelDat(); + } catch (Throwable ignored) { + + } + } + }); + } else { + worldNameField.setDisable(true); + } } BorderPane gameVersionPane = new BorderPane(); @@ -203,8 +222,7 @@ private void updateControls() { allowCheatsButton.setDisable(worldManagePage.isReadOnly()); Tag tag = dataTag.get("allowCommands"); - if (tag instanceof ByteTag) { - ByteTag byteTag = (ByteTag) tag; + if (tag instanceof ByteTag byteTag) { byte value = byteTag.getValue(); if (value == 0 || value == 1) { allowCheatsButton.setSelected(value == 1); @@ -226,8 +244,7 @@ private void updateControls() { generateFeaturesButton.setDisable(worldManagePage.isReadOnly()); Tag tag = worldGenSettings != null ? worldGenSettings.get("generate_features") : dataTag.get("MapFeatures"); - if (tag instanceof ByteTag) { - ByteTag byteTag = (ByteTag) tag; + if (tag instanceof ByteTag byteTag) { byte value = byteTag.getValue(); if (value == 0 || value == 1) { generateFeaturesButton.setSelected(value == 1); @@ -255,8 +272,7 @@ private void updateControls() { difficultyPane.setRight(difficultyBox); Tag tag = dataTag.get("Difficulty"); - if (tag instanceof ByteTag) { - ByteTag byteTag = (ByteTag) tag; + if (tag instanceof ByteTag byteTag) { Difficulty difficulty = Difficulty.of(byteTag.getValue()); if (difficulty != null) { difficultyBox.setValue(difficulty); @@ -274,16 +290,38 @@ private void updateControls() { } } + OptionToggleButton difficultyLockPane = new OptionToggleButton(); + { + difficultyLockPane.setTitle("难度锁定"); + difficultyLockPane.setDisable(worldManagePage.isReadOnly()); + + Tag tag = dataTag.get("DifficultyLocked"); + + if (tag instanceof ByteTag byteTag) { + byte value = byteTag.getValue(); + if (value == 0 || value == 1) { + difficultyLockPane.setSelected(value == 1); + difficultyLockPane.selectedProperty().addListener((o, oldValue, newValue) -> { + byteTag.setValue(newValue ? (byte) 1 : (byte) 0); + saveLevelDat(); + }); + } else { + difficultyLockPane.setDisable(true); + } + } else { + difficultyLockPane.setDisable(true); + } + } + basicInfo.getContent().setAll( worldNamePane, gameVersionPane, randomSeedPane, lastPlayedPane, timePane, - allowCheatsButton, generateFeaturesButton, difficultyPane); + allowCheatsButton, generateFeaturesButton, difficultyPane, difficultyLockPane); rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info.basic")), basicInfo); } Tag playerTag = dataTag.get("Player"); - if (playerTag instanceof CompoundTag) { - CompoundTag player = (CompoundTag) playerTag; + if (playerTag instanceof CompoundTag player) { ComponentList playerInfo = new ComponentList(); BorderPane locationPane = new BorderPane(); @@ -364,8 +402,7 @@ private void updateControls() { Tag hardcoreTag = dataTag.get("hardcore"); boolean isHardcore = hardcoreTag instanceof ByteTag && ((ByteTag) hardcoreTag).getValue() == 1; - if (tag instanceof IntTag) { - IntTag intTag = (IntTag) tag; + if (tag instanceof IntTag intTag) { GameType gameType = GameType.of(intTag.getValue(), isHardcore); if (gameType != null) { gameTypeBox.setValue(gameType); @@ -407,8 +444,7 @@ private void updateControls() { healthPane.setRight(healthField); Tag tag = player.get("Health"); - if (tag instanceof FloatTag) { - FloatTag floatTag = (FloatTag) tag; + if (tag instanceof FloatTag floatTag) { healthField.setText(new DecimalFormat("#").format(floatTag.getValue().floatValue())); healthField.textProperty().addListener((o, oldValue, newValue) -> { @@ -441,8 +477,7 @@ private void updateControls() { foodLevelPane.setRight(foodLevelField); Tag tag = player.get("foodLevel"); - if (tag instanceof IntTag) { - IntTag intTag = (IntTag) tag; + if (tag instanceof IntTag intTag) { foodLevelField.setText(String.valueOf(intTag.getValue())); foodLevelField.textProperty().addListener((o, oldValue, newValue) -> { @@ -475,8 +510,7 @@ private void updateControls() { xpLevelPane.setRight(xpLevelField); Tag tag = player.get("XpLevel"); - if (tag instanceof IntTag) { - IntTag intTag = (IntTag) tag; + if (tag instanceof IntTag intTag) { xpLevelField.setText(String.valueOf(intTag.getValue())); xpLevelField.textProperty().addListener((o, oldValue, newValue) -> { @@ -521,8 +555,8 @@ private static final class Dimension { final String name; static Dimension of(Tag tag) { - if (tag instanceof IntTag) { - switch (((IntTag) tag).getValue()) { + if (tag instanceof IntTag intTag) { + switch (intTag.getValue()) { case 0: return OVERWORLD; case 1: @@ -532,21 +566,14 @@ static Dimension of(Tag tag) { default: return null; } - } else if (tag instanceof StringTag) { - String id = ((StringTag) tag).getValue(); - switch (id) { - case "overworld": - case "minecraft:overworld": - return OVERWORLD; - case "the_nether": - case "minecraft:the_nether": - return THE_NETHER; - case "the_end": - case "minecraft:the_end": - return THE_END; - default: - return new Dimension(id); - } + } else if (tag instanceof StringTag stringTag) { + String id = stringTag.getValue(); + return switch (id) { + case "overworld", "minecraft:overworld" -> OVERWORLD; + case "the_nether", "minecraft:the_nether" -> THE_NETHER; + case "the_end", "minecraft:the_end" -> THE_END; + default -> new Dimension(id); + }; } else { return null; } @@ -557,8 +584,7 @@ private Dimension(String name) { } String formatPosition(Tag tag) { - if (tag instanceof ListTag) { - ListTag listTag = (ListTag) tag; + if (tag instanceof ListTag listTag) { if (listTag.size() != 3) return null; @@ -575,8 +601,7 @@ String formatPosition(Tag tag) { return null; } - if (tag instanceof IntArrayTag) { - IntArrayTag intArrayTag = (IntArrayTag) tag; + if (tag instanceof IntArrayTag intArrayTag) { int x = intArrayTag.getValue(0); int y = intArrayTag.getValue(1); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java index 5927c44143..911e66427f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java @@ -19,22 +19,12 @@ import javafx.scene.control.Control; import javafx.scene.control.Skin; -import javafx.stage.FileChooser; import org.jackhuang.hmcl.game.World; -import org.jackhuang.hmcl.game.WorldLockedException; -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.construct.MessageDialogPane.MessageType; -import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider; -import org.jackhuang.hmcl.util.StringUtils; -import org.jackhuang.hmcl.util.io.FileUtils; import java.nio.file.Path; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; - public final class WorldListItem extends Control { private final World world; private final Path backupsDir; @@ -56,34 +46,11 @@ public World getWorld() { } public void export() { - FileChooser fileChooser = new FileChooser(); - fileChooser.setTitle(i18n("world.export.title")); - fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("world"), "*.zip")); - fileChooser.setInitialFileName(world.getWorldName()); - Path file = FileUtils.toPath(fileChooser.showSaveDialog(Controllers.getStage())); - if (file == null) { - return; - } - - Controllers.getDecorator().startWizard(new SinglePageWizardProvider(controller -> new WorldExportPage(world, file, controller::onFinish))); + WorldManageUIUtils.export(world); } public void delete() { - Controllers.confirm( - i18n("button.remove.confirm"), - i18n("world.delete"), - () -> Task.runAsync(world::delete) - .whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception == null) { - parent.remove(this); - } else if (exception instanceof WorldLockedException) { - Controllers.dialog(i18n("world.locked.failed"), null, MessageType.WARNING); - } else { - Controllers.dialog(i18n("world.delete.failed", StringUtils.getStackTrace(exception)), null, MessageType.WARNING); - } - }).start(), - null - ); + WorldManageUIUtils.delete(world, () -> parent.remove(this)); } public void reveal() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java index c86befc77e..e4ceb246a1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java @@ -33,6 +33,7 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.util.ChunkBaseApp; +import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import java.io.IOException; @@ -64,7 +65,7 @@ public WorldManagePage(World world, Path backupsDir) { this.world = world; this.backupsDir = backupsDir; - this.state = new SimpleObjectProperty<>(State.fromTitle(i18n("world.manage.title", world.getWorldName()))); + this.state = new SimpleObjectProperty<>(State.fromTitle(i18n("world.manage.title", StringUtils.parseColorEscapes(world.getWorldName())))); this.header = new TabHeader(worldInfoTab, worldBackupsTab); worldInfoTab.setNodeSupplier(() -> new WorldInfoPage(this)); @@ -95,25 +96,39 @@ public WorldManagePage(World world, Path backupsDir) { AdvancedListBox toolbar = new AdvancedListBox(); if (ChunkBaseApp.isSupported(world)) { - PopupMenu popupMenu = new PopupMenu(); - JFXPopup popup = new JFXPopup(popupMenu); + PopupMenu chunkBasePopupMenu = new PopupMenu(); + JFXPopup chunkBasePopup = new JFXPopup(chunkBasePopupMenu); - popupMenu.getContent().addAll( - new IconedMenuItem(SVG.EXPLORE, i18n("world.chunkbase.seed_map"), () -> ChunkBaseApp.openSeedMap(world), popup), - new IconedMenuItem(SVG.VISIBILITY, i18n("world.chunkbase.stronghold"), () -> ChunkBaseApp.openStrongholdFinder(world), popup), - new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(world), popup) + PopupMenu managePopupMenu = new PopupMenu(); + JFXPopup managePopup = new JFXPopup(managePopupMenu); + + chunkBasePopupMenu.getContent().addAll( + new IconedMenuItem(SVG.EXPLORE, i18n("world.chunkbase.seed_map"), () -> ChunkBaseApp.openSeedMap(world), chunkBasePopup), + new IconedMenuItem(SVG.VISIBILITY, i18n("world.chunkbase.stronghold"), () -> ChunkBaseApp.openStrongholdFinder(world), chunkBasePopup), + new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(world), chunkBasePopup) ); if (GameVersionNumber.compare(world.getGameVersion(), "1.13") >= 0) { - popupMenu.getContent().add( - new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), () -> ChunkBaseApp.openEndCityFinder(world), popup)); + chunkBasePopupMenu.getContent().add( + new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), () -> ChunkBaseApp.openEndCityFinder(world), chunkBasePopup)); } + managePopupMenu.getContent().addAll( + new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> WorldManageUIUtils.export(world, sessionLockChannel), managePopup), + new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> WorldManageUIUtils.delete(world, () -> fireEvent(new PageCloseEvent()), sessionLockChannel), managePopup) + ); + toolbar.addNavigationDrawerItem(i18n("world.chunkbase"), SVG.EXPLORE, null, chunkBaseMenuItem -> chunkBaseMenuItem.setOnAction(e -> - popup.show(chunkBaseMenuItem, + chunkBasePopup.show(chunkBaseMenuItem, JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, chunkBaseMenuItem.getWidth(), 0))); + + toolbar.addNavigationDrawerItem("管理", SVG.MENU, null, managePopupMenuItem -> + managePopupMenuItem.setOnAction(e -> + managePopup.show(managePopupMenuItem, + JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, + managePopupMenuItem.getWidth(), 0))); } toolbar.addNavigationDrawerItem(i18n("settings.game.exploration"), SVG.FOLDER_OPEN, () -> FXUtils.openFolder(world.getFile()), null); @@ -126,6 +141,16 @@ public WorldManagePage(World world, Path backupsDir) { LOG.info("Acquired lock on world " + world.getFileName()); } catch (IOException ignored) { } + + this.addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated); + } + + private void onNavigated(Navigator.NavigationEvent event) { + try { + sessionLockChannel = world.lock(); + LOG.info("Acquired lock on world " + world.getFileName()); + } catch (IOException ignored) { + } } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java new file mode 100644 index 0000000000..f84647327f --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java @@ -0,0 +1,79 @@ +package org.jackhuang.hmcl.ui.versions; + +import javafx.stage.FileChooser; +import org.jackhuang.hmcl.game.World; +import org.jackhuang.hmcl.game.WorldLockedException; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.Controllers; +import org.jackhuang.hmcl.ui.construct.MessageDialogPane; +import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider; +import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.io.FileUtils; + +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.file.Path; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +public class WorldManageUIUtils { + public static void delete(World world, Runnable runnable) { + delete(world, runnable, null); + } + + public static void delete(World world, Runnable runnable, FileChannel sessionLockChannel) { + Controllers.confirm( + i18n("button.remove.confirm"), + i18n("world.delete"), + () -> Task.runAsync(() -> closeSessionLockChannel(world, sessionLockChannel)) + .whenComplete((exception) -> world.delete()) + .whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception == null) { + runnable.run(); + } else if (exception instanceof WorldLockedException) { + Controllers.dialog(i18n("world.locked.failed"), null, MessageDialogPane.MessageType.WARNING); + } else { + Controllers.dialog(i18n("world.delete.failed", StringUtils.getStackTrace(exception)), null, MessageDialogPane.MessageType.WARNING); + } + }).start(), + null + ); + } + + public static void export(World world) { + export(world, null); + } + + public static void export(World world, FileChannel sessionLockChannel) { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle(i18n("world.export.title")); + fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("world"), "*.zip")); + fileChooser.setInitialFileName(world.getWorldName()); + Path file = FileUtils.toPath(fileChooser.showSaveDialog(Controllers.getStage())); + if (file == null) { + return; + } + + try { + closeSessionLockChannel(world, sessionLockChannel); + } catch (IOException e) { + return; + } + + Controllers.getDecorator().startWizard(new SinglePageWizardProvider(controller -> new WorldExportPage(world, file, controller::onFinish))); + } + + private static void closeSessionLockChannel(World world, FileChannel sessionLockChannel) throws IOException { + if (sessionLockChannel != null) { + try { + sessionLockChannel.close(); + LOG.info("Releases the lock on world " + world.getFileName()); + } catch (IOException e) { + throw new IOException("Failed to close session lock channel", e); + } + } + } + +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java index 8d21dc25d7..6d99145fdd 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java @@ -96,6 +96,10 @@ public String getWorldName() { return worldName; } + public void setWorldName(String worldName) { + this.worldName = worldName; + } + public Path getLevelDatFile() { return file.resolve("level.dat"); } From 6c85f9d77906a54d0bb884b72147f556b90164cd Mon Sep 17 00:00:00 2001 From: mine_ Date: Tue, 18 Nov 2025 22:48:01 +0800 Subject: [PATCH 02/66] fix style --- .../org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java index f84647327f..0919c7ec16 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java @@ -19,6 +19,9 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; public class WorldManageUIUtils { + private WorldManageUIUtils() { + } + public static void delete(World world, Runnable runnable) { delete(world, runnable, null); } From cc099b0c09591855bdab7c6abd7ec0defdd2c9dd Mon Sep 17 00:00:00 2001 From: mine_ Date: Tue, 18 Nov 2025 22:50:43 +0800 Subject: [PATCH 03/66] fix style --- .../java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java index 0919c7ec16..2d791c1838 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java @@ -18,7 +18,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public class WorldManageUIUtils { +public final class WorldManageUIUtils { private WorldManageUIUtils() { } From 85777e455e6f7946e1bb0e44c919d54cb6ad6be6 Mon Sep 17 00:00:00 2001 From: mine_ Date: Wed, 19 Nov 2025 19:07:05 +0800 Subject: [PATCH 04/66] =?UTF-8?q?feat:=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java index 2d791c1838..8325c921dc 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java @@ -31,7 +31,7 @@ public static void delete(World world, Runnable runnable, FileChannel sessionLoc i18n("button.remove.confirm"), i18n("world.delete"), () -> Task.runAsync(() -> closeSessionLockChannel(world, sessionLockChannel)) - .whenComplete((exception) -> world.delete()) + .thenRunAsync(world::delete) .whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { runnable.run(); From eb07b8fd542ded630ace1ea07022061c9b84fb66 Mon Sep 17 00:00:00 2001 From: mine_ Date: Wed, 19 Nov 2025 20:58:07 +0800 Subject: [PATCH 05/66] =?UTF-8?q?feat:=E6=B7=BB=E5=8A=A0=E4=B8=96=E7=95=8C?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E6=98=BE=E7=A4=BA=E9=A1=B9=E5=92=8C=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E5=9B=BE=E6=A0=87=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/WorldInfoPage.java | 82 ++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index f63e6cf54b..6230f02e27 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -25,23 +25,33 @@ import javafx.collections.ObservableList; import javafx.geometry.Pos; import javafx.scene.Cursor; +import javafx.scene.SnapshotParameters; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.effect.BoxBlur; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.image.PixelReader; +import javafx.scene.image.WritableImage; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; +import javafx.stage.FileChooser; +import org.glavo.png.javafx.PNGJavaFXUtils; import org.jackhuang.hmcl.game.World; import org.jackhuang.hmcl.setting.Theme; 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.construct.*; +import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; import java.text.DecimalFormat; import java.time.Instant; import java.util.Arrays; @@ -59,6 +69,8 @@ public final class WorldInfoPage extends SpinnerPane { private final World world; private CompoundTag levelDat; + ImageView iconImageView = new ImageView(); + public WorldInfoPage(WorldManagePage worldManagePage) { this.worldManagePage = worldManagePage; this.world = worldManagePage.getWorld(); @@ -147,6 +159,19 @@ private void updateControls() { gameVersionPane.setRight(gameVersionLabel); } + BorderPane iconPane = new BorderPane(); + { + Label label = new Label("图标"); + BorderPane.setAlignment(label, Pos.CENTER_LEFT); + iconPane.setLeft(label); + + + FXUtils.limitSize(iconImageView, 32, 32); + iconImageView.setImage(world.getIcon() == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : world.getIcon()); + FXUtils.onClicked(iconImageView, this::replaceWorldIcon); + iconPane.setRight(iconImageView); + } + BorderPane randomSeedPane = new BorderPane(); { @@ -314,7 +339,7 @@ private void updateControls() { } basicInfo.getContent().setAll( - worldNamePane, gameVersionPane, randomSeedPane, lastPlayedPane, timePane, + worldNamePane, gameVersionPane, iconPane, randomSeedPane, lastPlayedPane, timePane, allowCheatsButton, generateFeaturesButton, difficultyPane, difficultyLockPane); rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info.basic")), basicInfo); @@ -658,4 +683,59 @@ public String toString() { return i18n("world.info.player.game_type." + name().toLowerCase(Locale.ROOT)); } } + + private void replaceWorldIcon() { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("选择图像"); + fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Image Files", "*.png")); + fileChooser.setInitialFileName("icon"); + + File file = fileChooser.showOpenDialog(Controllers.getStage()); + if (file == null) return; + + Image original = new Image(file.toURI().toString()); + + Image square = cropCenterSquare(original); + + Image finalImage; + if ((int) square.getWidth() == 64 && (int) square.getHeight() == 64) { + finalImage = square; + } else { + finalImage = resizeImage(square, 64, 64); + } + + Path output = world.getFile().resolve("icon.png"); + saveImage(finalImage, output); + } + + private Image cropCenterSquare(Image img) { + int w = (int) img.getWidth(); + int h = (int) img.getHeight(); + int size = Math.min(w, h); + int x = (w - size) / 2; + int y = (h - size) / 2; + + PixelReader reader = img.getPixelReader(); + WritableImage newImg = new WritableImage(reader, x, y, size, size); + return newImg; + } + + private Image resizeImage(Image img, int width, int height) { + ImageView view = new ImageView(img); + view.setFitWidth(width); + view.setFitHeight(height); + view.setPreserveRatio(false); + + SnapshotParameters params = new SnapshotParameters(); + return view.snapshot(params, null); + } + + private void saveImage(Image image, Path path) { + try { + PNGJavaFXUtils.writeImage(image, path); + iconImageView.setImage(image); + } catch (IOException e) { + LOG.warning(e.getMessage()); + } + } } From bec4e833b15d2909e904df7ee989728f6b4f5784 Mon Sep 17 00:00:00 2001 From: mine_ Date: Wed, 19 Nov 2025 21:17:04 +0800 Subject: [PATCH 06/66] =?UTF-8?q?feat:=E4=BC=98=E5=8C=96=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E4=B8=96=E7=95=8C=E5=9B=BE=E6=A0=87=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/WorldInfoPage.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index 6230f02e27..b091c89154 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -25,6 +25,7 @@ import javafx.collections.ObservableList; import javafx.geometry.Pos; import javafx.scene.Cursor; +import javafx.scene.Node; import javafx.scene.SnapshotParameters; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; @@ -165,11 +166,19 @@ private void updateControls() { BorderPane.setAlignment(label, Pos.CENTER_LEFT); iconPane.setLeft(label); - FXUtils.limitSize(iconImageView, 32, 32); iconImageView.setImage(world.getIcon() == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : world.getIcon()); - FXUtils.onClicked(iconImageView, this::replaceWorldIcon); - iconPane.setRight(iconImageView); + + Node editIcon = SVG.EDIT.createIcon(Theme.blackFill(), 12); + editIcon.setCursor(Cursor.HAND); + FXUtils.onClicked(editIcon, this::changeWorldIcon); + FXUtils.installFastTooltip(editIcon, "修改世界图标"); + + HBox hBox = new HBox(8); + hBox.setAlignment(Pos.CENTER_LEFT); + hBox.getChildren().addAll(editIcon, iconImageView); + + iconPane.setRight(hBox); } BorderPane randomSeedPane = new BorderPane(); @@ -684,7 +693,7 @@ public String toString() { } } - private void replaceWorldIcon() { + private void changeWorldIcon() { FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("选择图像"); fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Image Files", "*.png")); From a005ec77991750fde77366ace3d23bdced9da329 Mon Sep 17 00:00:00 2001 From: mine_ Date: Thu, 20 Nov 2025 08:16:57 +0800 Subject: [PATCH 07/66] =?UTF-8?q?feat:=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/versions/WorldInfoPage.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index b091c89154..c720373ae8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -32,7 +32,6 @@ import javafx.scene.effect.BoxBlur; import javafx.scene.image.Image; import javafx.scene.image.ImageView; -import javafx.scene.image.PixelReader; import javafx.scene.image.WritableImage; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; @@ -697,20 +696,20 @@ private void changeWorldIcon() { FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("选择图像"); fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Image Files", "*.png")); - fileChooser.setInitialFileName("icon"); + fileChooser.setInitialFileName("icon.png"); File file = fileChooser.showOpenDialog(Controllers.getStage()); if (file == null) return; Image original = new Image(file.toURI().toString()); - Image square = cropCenterSquare(original); + Image squareImage = cropCenterSquare(original); Image finalImage; - if ((int) square.getWidth() == 64 && (int) square.getHeight() == 64) { - finalImage = square; + if ((int) squareImage.getWidth() == 64 && (int) squareImage.getHeight() == 64) { + finalImage = squareImage; } else { - finalImage = resizeImage(square, 64, 64); + finalImage = resizeImage(squareImage, 64, 64); } Path output = world.getFile().resolve("icon.png"); @@ -724,9 +723,7 @@ private Image cropCenterSquare(Image img) { int x = (w - size) / 2; int y = (h - size) / 2; - PixelReader reader = img.getPixelReader(); - WritableImage newImg = new WritableImage(reader, x, y, size, size); - return newImg; + return new WritableImage(img.getPixelReader(), x, y, size, size); } private Image resizeImage(Image img, int width, int height) { From c6ecfc597418fd61f4bc900fa90ca8d50ca8ce91 Mon Sep 17 00:00:00 2001 From: mine_ Date: Thu, 20 Nov 2025 09:23:47 +0800 Subject: [PATCH 08/66] =?UTF-8?q?feat:=E4=BF=AE=E6=94=B9=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E4=B9=8B=E5=89=8D=E8=BF=9B=E8=A1=8C=E5=BC=B9=E7=AA=97=E6=8F=90?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/ui/versions/WorldInfoPage.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index c720373ae8..ec4cbaf4b4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -170,9 +170,15 @@ private void updateControls() { Node editIcon = SVG.EDIT.createIcon(Theme.blackFill(), 12); editIcon.setCursor(Cursor.HAND); - FXUtils.onClicked(editIcon, this::changeWorldIcon); + FXUtils.onClicked(editIcon, () -> Controllers.confirm( + "你需要提供一个分辨率为64×64,格式为PNG的图片,如果不是,HMCL将会将图片进行裁切并将分辨率修改为64×64","更改世界图标", MessageDialogPane.MessageType.INFO, + this::changeWorldIcon, + null + )); FXUtils.installFastTooltip(editIcon, "修改世界图标"); + + HBox hBox = new HBox(8); hBox.setAlignment(Pos.CENTER_LEFT); hBox.getChildren().addAll(editIcon, iconImageView); From aa1be7dd9642a4660c2cdb8935c2b608abb635e9 Mon Sep 17 00:00:00 2001 From: mine_ Date: Thu, 20 Nov 2025 15:29:43 +0800 Subject: [PATCH 09/66] =?UTF-8?q?feat:=E6=9B=B4=E6=96=B0=E6=96=87=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index ec4cbaf4b4..d4295ce1fd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -171,13 +171,11 @@ private void updateControls() { Node editIcon = SVG.EDIT.createIcon(Theme.blackFill(), 12); editIcon.setCursor(Cursor.HAND); FXUtils.onClicked(editIcon, () -> Controllers.confirm( - "你需要提供一个分辨率为64×64,格式为PNG的图片,如果不是,HMCL将会将图片进行裁切并将分辨率修改为64×64","更改世界图标", MessageDialogPane.MessageType.INFO, + "你需要提供一个分辨率为64×64,格式为PNG的图片,如果不是,HMCL将会将图片进行裁切并将分辨率修改为64×64", "更改世界图标", MessageDialogPane.MessageType.INFO, this::changeWorldIcon, null )); - FXUtils.installFastTooltip(editIcon, "修改世界图标"); - - + FXUtils.installFastTooltip(editIcon, "更改世界图标"); HBox hBox = new HBox(8); hBox.setAlignment(Pos.CENTER_LEFT); From 34adf976f4a4aff6fdcbc14b25581778b5102a66 Mon Sep 17 00:00:00 2001 From: mine_ Date: Thu, 20 Nov 2025 22:23:54 +0800 Subject: [PATCH 10/66] =?UTF-8?q?feat:=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=EF=BC=8C=E6=B7=BB=E5=8A=A0i18n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/WorldInfoPage.java | 131 ++++++------------ .../resources/assets/lang/I18N.properties | 2 + .../resources/assets/lang/I18N_zh.properties | 2 + .../assets/lang/I18N_zh_CN.properties | 2 + 4 files changed, 51 insertions(+), 86 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index d4295ce1fd..4f1dcbaaa9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -47,6 +47,7 @@ import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.*; +import org.jetbrains.annotations.PropertyKey; import java.io.File; import java.io.IOException; @@ -116,15 +117,9 @@ private void updateControls() { { BorderPane worldNamePane = new BorderPane(); { - Label label = new Label(i18n("world.name")); - worldNamePane.setLeft(label); - BorderPane.setAlignment(label, Pos.CENTER_LEFT); - + setLeftLabel(worldNamePane, "world.name"); JFXTextField worldNameField = new JFXTextField(); - worldNameField.setDisable(worldManagePage.isDisable()); - worldNameField.setPrefWidth(200); - BorderPane.setAlignment(worldNameField, Pos.CENTER_RIGHT); - worldNamePane.setRight(worldNameField); + setRightTextField(worldNamePane, worldNameField, 200); Tag tag = dataTag.get("LevelName"); if (tag instanceof StringTag stringTag) { @@ -148,27 +143,21 @@ private void updateControls() { BorderPane gameVersionPane = new BorderPane(); { - Label label = new Label(i18n("world.info.game_version")); - BorderPane.setAlignment(label, Pos.CENTER_LEFT); - gameVersionPane.setLeft(label); - + setLeftLabel(gameVersionPane, "world.info.game_version"); Label gameVersionLabel = new Label(); - FXUtils.copyOnDoubleClick(gameVersionLabel); gameVersionLabel.setText(world.getGameVersion()); - BorderPane.setAlignment(gameVersionLabel, Pos.CENTER_RIGHT); - gameVersionPane.setRight(gameVersionLabel); + setRightTextLabel(gameVersionPane, gameVersionLabel); } BorderPane iconPane = new BorderPane(); { - Label label = new Label("图标"); - BorderPane.setAlignment(label, Pos.CENTER_LEFT); - iconPane.setLeft(label); + setLeftLabel(iconPane, "world.icon"); FXUtils.limitSize(iconImageView, 32, 32); iconImageView.setImage(world.getIcon() == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : world.getIcon()); Node editIcon = SVG.EDIT.createIcon(Theme.blackFill(), 12); + editIcon.setDisable(worldManagePage.isReadOnly()); editIcon.setCursor(Cursor.HAND); FXUtils.onClicked(editIcon, () -> Controllers.confirm( "你需要提供一个分辨率为64×64,格式为PNG的图片,如果不是,HMCL将会将图片进行裁切并将分辨率修改为64×64", "更改世界图标", MessageDialogPane.MessageType.INFO, @@ -224,27 +213,18 @@ private void updateControls() { BorderPane lastPlayedPane = new BorderPane(); { - Label label = new Label(i18n("world.info.last_played")); - BorderPane.setAlignment(label, Pos.CENTER_LEFT); - lastPlayedPane.setLeft(label); - + setLeftLabel(lastPlayedPane, "world.info.last_played"); Label lastPlayedLabel = new Label(); - FXUtils.copyOnDoubleClick(lastPlayedLabel); lastPlayedLabel.setText(formatDateTime(Instant.ofEpochMilli(world.getLastPlayed()))); - BorderPane.setAlignment(lastPlayedLabel, Pos.CENTER_RIGHT); - lastPlayedPane.setRight(lastPlayedLabel); + setRightTextLabel(lastPlayedPane, lastPlayedLabel); } BorderPane timePane = new BorderPane(); { - Label label = new Label(i18n("world.info.time")); - BorderPane.setAlignment(label, Pos.CENTER_LEFT); - timePane.setLeft(label); + setLeftLabel(timePane, "world.info.time"); Label timeLabel = new Label(); - FXUtils.copyOnDoubleClick(timeLabel); - BorderPane.setAlignment(timeLabel, Pos.CENTER_RIGHT); - timePane.setRight(timeLabel); + setRightTextLabel(timePane, timeLabel); Tag tag = dataTag.get("Time"); if (tag instanceof LongTag) { @@ -299,9 +279,7 @@ private void updateControls() { BorderPane difficultyPane = new BorderPane(); { - Label label = new Label(i18n("world.info.difficulty")); - BorderPane.setAlignment(label, Pos.CENTER_LEFT); - difficultyPane.setLeft(label); + setLeftLabel(difficultyPane, "world.info.difficulty"); JFXComboBox difficultyBox = new JFXComboBox<>(Difficulty.items); difficultyBox.setDisable(worldManagePage.isReadOnly()); @@ -329,7 +307,7 @@ private void updateControls() { OptionToggleButton difficultyLockPane = new OptionToggleButton(); { - difficultyLockPane.setTitle("难度锁定"); + difficultyLockPane.setTitle(i18n("world.info.difficultyLock")); difficultyLockPane.setDisable(worldManagePage.isReadOnly()); Tag tag = dataTag.get("DifficultyLocked"); @@ -363,14 +341,9 @@ private void updateControls() { BorderPane locationPane = new BorderPane(); { - Label label = new Label(i18n("world.info.player.location")); - BorderPane.setAlignment(label, Pos.CENTER_LEFT); - locationPane.setLeft(label); - + setLeftLabel(locationPane, "world.info.player.location"); Label locationLabel = new Label(); - FXUtils.copyOnDoubleClick(locationLabel); - BorderPane.setAlignment(locationLabel, Pos.CENTER_RIGHT); - locationPane.setRight(locationLabel); + setRightTextLabel(locationPane, locationLabel); Dimension dim = Dimension.of(player.get("Dimension")); if (dim != null) { @@ -382,14 +355,9 @@ private void updateControls() { BorderPane lastDeathLocationPane = new BorderPane(); { - Label label = new Label(i18n("world.info.player.last_death_location")); - BorderPane.setAlignment(label, Pos.CENTER_LEFT); - lastDeathLocationPane.setLeft(label); - + setLeftLabel(lastDeathLocationPane, "world.info.player.last_death_location"); Label lastDeathLocationLabel = new Label(); - FXUtils.copyOnDoubleClick(lastDeathLocationLabel); - BorderPane.setAlignment(lastDeathLocationLabel, Pos.CENTER_RIGHT); - lastDeathLocationPane.setRight(lastDeathLocationLabel); + setRightTextLabel(lastDeathLocationPane, lastDeathLocationLabel); Tag tag = player.get("LastDeathLocation"); if (tag instanceof CompoundTag) { @@ -404,14 +372,9 @@ private void updateControls() { BorderPane spawnPane = new BorderPane(); { - Label label = new Label(i18n("world.info.player.spawn")); - BorderPane.setAlignment(label, Pos.CENTER_LEFT); - spawnPane.setLeft(label); - + setLeftLabel(spawnPane, "world.info.player.spawn"); Label spawnLabel = new Label(); - FXUtils.copyOnDoubleClick(spawnLabel); - BorderPane.setAlignment(spawnLabel, Pos.CENTER_RIGHT); - spawnPane.setRight(spawnLabel); + setRightTextLabel(spawnPane, spawnLabel); Dimension dim = Dimension.of(player.get("SpawnDimension")); if (dim != null) { @@ -426,9 +389,7 @@ private void updateControls() { BorderPane playerGameTypePane = new BorderPane(); { - Label label = new Label(i18n("world.info.player.game_type")); - BorderPane.setAlignment(label, Pos.CENTER_LEFT); - playerGameTypePane.setLeft(label); + setLeftLabel(playerGameTypePane, "world.info.player.game_type"); JFXComboBox gameTypeBox = new JFXComboBox<>(GameType.items); gameTypeBox.setDisable(worldManagePage.isReadOnly()); @@ -469,16 +430,9 @@ private void updateControls() { BorderPane healthPane = new BorderPane(); { - Label label = new Label(i18n("world.info.player.health")); - BorderPane.setAlignment(label, Pos.CENTER_LEFT); - healthPane.setLeft(label); - + setLeftLabel(healthPane, "world.info.player.health"); JFXTextField healthField = new JFXTextField(); - healthField.setDisable(worldManagePage.isReadOnly()); - healthField.setPrefWidth(50); - healthField.setAlignment(Pos.CENTER_RIGHT); - BorderPane.setAlignment(healthField, Pos.CENTER_RIGHT); - healthPane.setRight(healthField); + setRightTextField(healthPane, healthField, 50); Tag tag = player.get("Health"); if (tag instanceof FloatTag floatTag) { @@ -502,16 +456,9 @@ private void updateControls() { BorderPane foodLevelPane = new BorderPane(); { - Label label = new Label(i18n("world.info.player.food_level")); - BorderPane.setAlignment(label, Pos.CENTER_LEFT); - foodLevelPane.setLeft(label); - + setLeftLabel(foodLevelPane, "world.info.player.food_level"); JFXTextField foodLevelField = new JFXTextField(); - foodLevelField.setDisable(worldManagePage.isReadOnly()); - foodLevelField.setPrefWidth(50); - foodLevelField.setAlignment(Pos.CENTER_RIGHT); - BorderPane.setAlignment(foodLevelField, Pos.CENTER_RIGHT); - foodLevelPane.setRight(foodLevelField); + setRightTextField(foodLevelPane, foodLevelField, 50); Tag tag = player.get("foodLevel"); if (tag instanceof IntTag intTag) { @@ -535,16 +482,9 @@ private void updateControls() { BorderPane xpLevelPane = new BorderPane(); { - Label label = new Label(i18n("world.info.player.xp_level")); - BorderPane.setAlignment(label, Pos.CENTER_LEFT); - xpLevelPane.setLeft(label); - + setLeftLabel(xpLevelPane, "world.info.player.xp_level"); JFXTextField xpLevelField = new JFXTextField(); - xpLevelField.setDisable(worldManagePage.isReadOnly()); - xpLevelField.setPrefWidth(50); - xpLevelField.setAlignment(Pos.CENTER_RIGHT); - BorderPane.setAlignment(xpLevelField, Pos.CENTER_RIGHT); - xpLevelPane.setRight(xpLevelField); + setRightTextField(xpLevelPane, xpLevelField, 50); Tag tag = player.get("XpLevel"); if (tag instanceof IntTag intTag) { @@ -575,6 +515,25 @@ private void updateControls() { } } + private void setLeftLabel(BorderPane borderPane, @PropertyKey(resourceBundle = "assets.lang.I18N") String key) { + Label label = new Label(i18n(key)); + BorderPane.setAlignment(label, Pos.CENTER_LEFT); + borderPane.setLeft(label); + } + + private void setRightTextField(BorderPane borderPane, JFXTextField textField, int perfWidth) { + textField.setDisable(worldManagePage.isDisable()); + textField.setPrefWidth(perfWidth); + BorderPane.setAlignment(textField, Pos.CENTER_RIGHT); + borderPane.setRight(textField); + } + + private void setRightTextLabel(BorderPane borderPane, Label label) { + FXUtils.copyOnDoubleClick(label); + BorderPane.setAlignment(label, Pos.CENTER_RIGHT); + borderPane.setRight(label); + } + private void saveLevelDat() { LOG.info("Saving level.dat of world " + world.getWorldName()); try { diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 32f4abca59..a12be17124 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1139,6 +1139,7 @@ world.export.location=Save As world.export.wizard=Export World "%s" world.extension=World Archive world.game_version=Game Version +world.icon=World Icon world.import.already_exists=This world already exists. world.import.choose=Choose world archive you want to import world.import.failed=Failed to import this world: %s @@ -1153,6 +1154,7 @@ world.info.difficulty.peaceful=Peaceful world.info.difficulty.easy=Easy world.info.difficulty.normal=Normal world.info.difficulty.hard=Hard +world.info.difficultyLock=Lock Difficulty world.info.failed=Failed to read the world info world.info.game_version=Game Version world.info.last_played=Last Played diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 71882ac121..fb4a78ed58 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -935,6 +935,7 @@ world.export.title=選取該世界的儲存位置 world.export.location=儲存到 world.export.wizard=匯出世界「%s」 world.extension=存檔壓縮檔 +world.icon=世界圖標 world.import.already_exists=此世界已經存在 world.import.choose=選取要匯入的存檔壓縮檔 world.import.failed=無法匯入此世界: %s @@ -949,6 +950,7 @@ world.info.difficulty.peaceful=和平 world.info.difficulty.easy=簡單 world.info.difficulty.normal=普通 world.info.difficulty.hard=困難 +world.info.difficultyLock=鎖定難度 world.info.failed=讀取世界資訊失敗 world.info.game_version=遊戲版本 world.info.last_played=上一次遊戲時間 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index d4a5e9c68c..86bd1f6e1d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -946,6 +946,7 @@ world.export.location=保存到 world.export.wizard=导出世界“%s” world.extension=世界压缩包 world.game_version=游戏版本 +world.icon=世界图标 world.import.already_exists=此世界已经存在 world.import.choose=选择要导入的存档压缩包 world.import.failed=无法导入此世界:%s @@ -960,6 +961,7 @@ world.info.difficulty.peaceful=和平 world.info.difficulty.easy=简单 world.info.difficulty.normal=普通 world.info.difficulty.hard=困难 +world.info.difficultyLock=锁定难度 world.info.failed=读取世界信息失败 world.info.game_version=游戏版本 world.info.last_played=上一次游戏时间 From 32905b3d9ebcf55429ce705e88db9f05200db6c5 Mon Sep 17 00:00:00 2001 From: mine_ Date: Thu, 20 Nov 2025 22:25:35 +0800 Subject: [PATCH 11/66] =?UTF-8?q?feat:=E6=9B=B4=E6=96=B0i18n=E9=94=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java | 2 +- HMCL/src/main/resources/assets/lang/I18N.properties | 2 +- HMCL/src/main/resources/assets/lang/I18N_zh.properties | 2 +- HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index 4f1dcbaaa9..a92c4f7aac 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -307,7 +307,7 @@ private void updateControls() { OptionToggleButton difficultyLockPane = new OptionToggleButton(); { - difficultyLockPane.setTitle(i18n("world.info.difficultyLock")); + difficultyLockPane.setTitle(i18n("world.info.difficulty_lock")); difficultyLockPane.setDisable(worldManagePage.isReadOnly()); Tag tag = dataTag.get("DifficultyLocked"); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index a12be17124..b731111c4f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1154,7 +1154,7 @@ world.info.difficulty.peaceful=Peaceful world.info.difficulty.easy=Easy world.info.difficulty.normal=Normal world.info.difficulty.hard=Hard -world.info.difficultyLock=Lock Difficulty +world.info.difficulty_lock=Lock Difficulty world.info.failed=Failed to read the world info world.info.game_version=Game Version world.info.last_played=Last Played diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index fb4a78ed58..3e60cb195b 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -950,7 +950,7 @@ world.info.difficulty.peaceful=和平 world.info.difficulty.easy=簡單 world.info.difficulty.normal=普通 world.info.difficulty.hard=困難 -world.info.difficultyLock=鎖定難度 +world.info.difficulty_lock=鎖定難度 world.info.failed=讀取世界資訊失敗 world.info.game_version=遊戲版本 world.info.last_played=上一次遊戲時間 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 86bd1f6e1d..c1453ecf03 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -961,7 +961,7 @@ world.info.difficulty.peaceful=和平 world.info.difficulty.easy=简单 world.info.difficulty.normal=普通 world.info.difficulty.hard=困难 -world.info.difficultyLock=锁定难度 +world.info.difficult_lock=锁定难度 world.info.failed=读取世界信息失败 world.info.game_version=游戏版本 world.info.last_played=上一次游戏时间 From f5d19d230f4bbd62c7591c7f65902846c99d5c50 Mon Sep 17 00:00:00 2001 From: mine_ Date: Thu, 20 Nov 2025 22:30:39 +0800 Subject: [PATCH 12/66] =?UTF-8?q?feat:=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/ui/versions/WorldInfoPage.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index a92c4f7aac..ff05f2e9e3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -360,10 +360,10 @@ private void updateControls() { setRightTextLabel(lastDeathLocationPane, lastDeathLocationLabel); Tag tag = player.get("LastDeathLocation"); - if (tag instanceof CompoundTag) { - Dimension dim = Dimension.of(((CompoundTag) tag).get("dimension")); + if (tag instanceof CompoundTag compoundTag) { + Dimension dim = Dimension.of(compoundTag.get("dimension")); if (dim != null) { - String posString = dim.formatPosition(((CompoundTag) tag).get("pos")); + String posString = dim.formatPosition(compoundTag.get("pos")); if (posString != null) lastDeathLocationLabel.setText(posString); } @@ -382,8 +382,8 @@ private void updateControls() { Tag y = player.get("SpawnY"); Tag z = player.get("SpawnZ"); - if (x instanceof IntTag && y instanceof IntTag && z instanceof IntTag) - spawnLabel.setText(dim.formatPosition(((IntTag) x).getValue(), ((IntTag) y).getValue(), ((IntTag) z).getValue())); + if (x instanceof IntTag intX && y instanceof IntTag intY && z instanceof IntTag intZ) + spawnLabel.setText(dim.formatPosition(intX.getValue(), intY.getValue(), intZ.getValue())); } } From 34b43cdf1f7150c3ed601527ccb2f2f8cbec6042 Mon Sep 17 00:00:00 2001 From: mine_ Date: Thu, 20 Nov 2025 22:44:36 +0800 Subject: [PATCH 13/66] =?UTF-8?q?feat:=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/WorldInfoPage.java | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index ff05f2e9e3..838f00b0cc 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -132,7 +132,6 @@ private void updateControls() { world.setWorldName(newValue); saveLevelDat(); } catch (Throwable ignored) { - } } }); @@ -543,25 +542,19 @@ private void saveLevelDat() { } } - private static final class Dimension { + private record Dimension(String name) { static final Dimension OVERWORLD = new Dimension(null); static final Dimension THE_NETHER = new Dimension(i18n("world.info.dimension.the_nether")); static final Dimension THE_END = new Dimension(i18n("world.info.dimension.the_end")); - final String name; - static Dimension of(Tag tag) { if (tag instanceof IntTag intTag) { - switch (intTag.getValue()) { - case 0: - return OVERWORLD; - case 1: - return THE_NETHER; - case 2: - return THE_END; - default: - return null; - } + return switch (intTag.getValue()) { + case 0 -> OVERWORLD; + case 1 -> THE_NETHER; + case 2 -> THE_END; + default -> null; + }; } else if (tag instanceof StringTag stringTag) { String id = stringTag.getValue(); return switch (id) { @@ -575,10 +568,6 @@ static Dimension of(Tag tag) { } } - private Dimension(String name) { - this.name = name; - } - String formatPosition(Tag tag) { if (tag instanceof ListTag listTag) { if (listTag.size() != 3) From 28c2a2e370c4e7410e23ff05515c03aa77e87438 Mon Sep 17 00:00:00 2001 From: mine_ Date: Thu, 20 Nov 2025 22:53:09 +0800 Subject: [PATCH 14/66] =?UTF-8?q?feat:=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/WorldInfoPage.java | 90 ++++++++++--------- 1 file changed, 50 insertions(+), 40 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index 838f00b0cc..4195ec311a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -57,6 +57,7 @@ import java.time.Instant; import java.util.Arrays; import java.util.Locale; +import java.util.concurrent.Callable; import static org.jackhuang.hmcl.util.i18n.I18n.formatDateTime; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -144,8 +145,7 @@ private void updateControls() { { setLeftLabel(gameVersionPane, "world.info.game_version"); Label gameVersionLabel = new Label(); - gameVersionLabel.setText(world.getGameVersion()); - setRightTextLabel(gameVersionPane, gameVersionLabel); + setRightTextLabel(gameVersionPane, gameVersionLabel, world::getGameVersion); } BorderPane iconPane = new BorderPane(); @@ -214,8 +214,8 @@ private void updateControls() { { setLeftLabel(lastPlayedPane, "world.info.last_played"); Label lastPlayedLabel = new Label(); - lastPlayedLabel.setText(formatDateTime(Instant.ofEpochMilli(world.getLastPlayed()))); - setRightTextLabel(lastPlayedPane, lastPlayedLabel); + //lastPlayedLabel.setText(formatDateTime(Instant.ofEpochMilli(world.getLastPlayed()))); + setRightTextLabel(lastPlayedPane, lastPlayedLabel, () -> formatDateTime(Instant.ofEpochMilli(world.getLastPlayed()))); } BorderPane timePane = new BorderPane(); @@ -223,13 +223,15 @@ private void updateControls() { setLeftLabel(timePane, "world.info.time"); Label timeLabel = new Label(); - setRightTextLabel(timePane, timeLabel); - - Tag tag = dataTag.get("Time"); - if (tag instanceof LongTag) { - long days = ((LongTag) tag).getValue() / 24000; - timeLabel.setText(i18n("world.info.time.format", days)); - } + setRightTextLabel(timePane, timeLabel, () -> { + Tag tag = dataTag.get("Time"); + if (tag instanceof LongTag) { + long days = ((LongTag) tag).getValue() / 24000; + return i18n("world.info.time.format", days); + } else { + return ""; + } + }); } OptionToggleButton allowCheatsButton = new OptionToggleButton(); @@ -342,48 +344,52 @@ private void updateControls() { { setLeftLabel(locationPane, "world.info.player.location"); Label locationLabel = new Label(); - setRightTextLabel(locationPane, locationLabel); - - Dimension dim = Dimension.of(player.get("Dimension")); - if (dim != null) { - String posString = dim.formatPosition(player.get("Pos")); - if (posString != null) - locationLabel.setText(posString); - } + setRightTextLabel(locationPane, locationLabel, () -> { + Dimension dim = Dimension.of(player.get("Dimension")); + if (dim != null) { + String posString = dim.formatPosition(player.get("Pos")); + if (posString != null) + return posString; + } + return ""; + }); } BorderPane lastDeathLocationPane = new BorderPane(); { setLeftLabel(lastDeathLocationPane, "world.info.player.last_death_location"); Label lastDeathLocationLabel = new Label(); - setRightTextLabel(lastDeathLocationPane, lastDeathLocationLabel); - - Tag tag = player.get("LastDeathLocation"); - if (tag instanceof CompoundTag compoundTag) { - Dimension dim = Dimension.of(compoundTag.get("dimension")); - if (dim != null) { - String posString = dim.formatPosition(compoundTag.get("pos")); - if (posString != null) - lastDeathLocationLabel.setText(posString); + setRightTextLabel(lastDeathLocationPane, lastDeathLocationLabel, () -> { + Tag tag = player.get("LastDeathLocation"); + if (tag instanceof CompoundTag compoundTag) { + Dimension dim = Dimension.of(compoundTag.get("dimension")); + if (dim != null) { + String posString = dim.formatPosition(compoundTag.get("pos")); + if (posString != null) + return posString; + } } - } + return ""; + }); + } BorderPane spawnPane = new BorderPane(); { setLeftLabel(spawnPane, "world.info.player.spawn"); Label spawnLabel = new Label(); - setRightTextLabel(spawnPane, spawnLabel); - - Dimension dim = Dimension.of(player.get("SpawnDimension")); - if (dim != null) { - Tag x = player.get("SpawnX"); - Tag y = player.get("SpawnY"); - Tag z = player.get("SpawnZ"); + setRightTextLabel(spawnPane, spawnLabel, () -> { + Dimension dim = Dimension.of(player.get("SpawnDimension")); + if (dim != null) { + Tag x = player.get("SpawnX"); + Tag y = player.get("SpawnY"); + Tag z = player.get("SpawnZ"); - if (x instanceof IntTag intX && y instanceof IntTag intY && z instanceof IntTag intZ) - spawnLabel.setText(dim.formatPosition(intX.getValue(), intY.getValue(), intZ.getValue())); - } + if (x instanceof IntTag intX && y instanceof IntTag intY && z instanceof IntTag intZ) + return dim.formatPosition(intX.getValue(), intY.getValue(), intZ.getValue()); + } + return ""; + }); } BorderPane playerGameTypePane = new BorderPane(); @@ -527,9 +533,13 @@ private void setRightTextField(BorderPane borderPane, JFXTextField textField, in borderPane.setRight(textField); } - private void setRightTextLabel(BorderPane borderPane, Label label) { + private void setRightTextLabel(BorderPane borderPane, Label label, Callable setNameCall) { FXUtils.copyOnDoubleClick(label); BorderPane.setAlignment(label, Pos.CENTER_RIGHT); + try { + label.setText(setNameCall.call()); + } catch (Throwable ignored) { + } borderPane.setRight(label); } From 90235be21d99f3466a6c8f8267d60152896eb0c7 Mon Sep 17 00:00:00 2001 From: mine_ Date: Thu, 20 Nov 2025 23:47:17 +0800 Subject: [PATCH 15/66] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=97=A0?= =?UTF-8?q?=E6=B3=95=E8=8E=B7=E5=8F=9625w07a=E4=B9=8B=E5=90=8E=E7=9A=84?= =?UTF-8?q?=E7=8E=A9=E5=AE=B6=E5=9D=90=E6=A0=87=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/versions/WorldInfoPage.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index 4195ec311a..17a67e03f0 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -379,14 +379,23 @@ private void updateControls() { setLeftLabel(spawnPane, "world.info.player.spawn"); Label spawnLabel = new Label(); setRightTextLabel(spawnPane, spawnLabel, () -> { + Dimension dim = Dimension.of(player.get("SpawnDimension")); - if (dim != null) { + if (dim != null) {//before 25w07a Tag x = player.get("SpawnX"); Tag y = player.get("SpawnY"); Tag z = player.get("SpawnZ"); if (x instanceof IntTag intX && y instanceof IntTag intY && z instanceof IntTag intZ) return dim.formatPosition(intX.getValue(), intY.getValue(), intZ.getValue()); + } else {//after 25w07a + CompoundTag respawnTag = player.get("respawn"); + dim = Dimension.of(respawnTag.get("dimension")); + Tag posTag = respawnTag.get("pos"); + + if (posTag instanceof IntArrayTag intArrayTag) { + return dim.formatPosition(intArrayTag.getValue(0), intArrayTag.getValue(1), intArrayTag.getValue(2)); + } } return ""; }); @@ -538,7 +547,8 @@ private void setRightTextLabel(BorderPane borderPane, Label label, Callable Date: Thu, 20 Nov 2025 23:52:34 +0800 Subject: [PATCH 16/66] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Di18n=E9=94=AE?= =?UTF-8?q?=E5=90=8D=E9=94=99=E8=AF=AF=EF=BC=8C=E6=9B=B4=E6=94=B9=E7=B9=81?= =?UTF-8?q?=E4=B8=AD=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HMCL/src/main/resources/assets/lang/I18N_zh.properties | 4 ++-- HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 3e60cb195b..5d419ba2a2 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -935,7 +935,7 @@ world.export.title=選取該世界的儲存位置 world.export.location=儲存到 world.export.wizard=匯出世界「%s」 world.extension=存檔壓縮檔 -world.icon=世界圖標 +world.icon=世界圖示 world.import.already_exists=此世界已經存在 world.import.choose=選取要匯入的存檔壓縮檔 world.import.failed=無法匯入此世界: %s @@ -950,7 +950,7 @@ world.info.difficulty.peaceful=和平 world.info.difficulty.easy=簡單 world.info.difficulty.normal=普通 world.info.difficulty.hard=困難 -world.info.difficulty_lock=鎖定難度 +world.info.difficulty_lock=鎖定難易度 world.info.failed=讀取世界資訊失敗 world.info.game_version=遊戲版本 world.info.last_played=上一次遊戲時間 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index c1453ecf03..3114308ea2 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -961,7 +961,7 @@ world.info.difficulty.peaceful=和平 world.info.difficulty.easy=简单 world.info.difficulty.normal=普通 world.info.difficulty.hard=困难 -world.info.difficult_lock=锁定难度 +world.info.difficulty_lock=锁定难度 world.info.failed=读取世界信息失败 world.info.game_version=游戏版本 world.info.last_played=上一次游戏时间 From 2d20e7dce0434a367f92c70285f79c1389f9146e Mon Sep 17 00:00:00 2001 From: mine_ Date: Thu, 20 Nov 2025 23:55:22 +0800 Subject: [PATCH 17/66] fix style --- .../java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index 17a67e03f0..5d2616c98c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -381,14 +381,14 @@ private void updateControls() { setRightTextLabel(spawnPane, spawnLabel, () -> { Dimension dim = Dimension.of(player.get("SpawnDimension")); - if (dim != null) {//before 25w07a + if (dim != null) { //before 25w07a Tag x = player.get("SpawnX"); Tag y = player.get("SpawnY"); Tag z = player.get("SpawnZ"); if (x instanceof IntTag intX && y instanceof IntTag intY && z instanceof IntTag intZ) return dim.formatPosition(intX.getValue(), intY.getValue(), intZ.getValue()); - } else {//after 25w07a + } else { //after 25w07a CompoundTag respawnTag = player.get("respawn"); dim = Dimension.of(respawnTag.get("dimension")); Tag posTag = respawnTag.get("pos"); From 66ec94a07a04cb61995e2c4ff11c23b6e98f35be Mon Sep 17 00:00:00 2001 From: mine_ Date: Fri, 21 Nov 2025 19:44:38 +0800 Subject: [PATCH 18/66] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=83=A8?= =?UTF-8?q?=E5=88=86=E9=80=BB=E8=BE=91=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/WorldInfoPage.java | 2 +- .../hmcl/ui/versions/WorldManagePage.java | 27 ++++++++++--------- .../hmcl/ui/versions/WorldManageUIUtils.java | 2 -- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index 5d2616c98c..c23fc2c7e9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -548,7 +548,7 @@ private void setRightTextLabel(BorderPane borderPane, Label label, Callable ChunkBaseApp.openSeedMap(world), chunkBasePopup), @@ -111,25 +109,28 @@ public WorldManagePage(World world, Path backupsDir) { new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), () -> ChunkBaseApp.openEndCityFinder(world), chunkBasePopup)); } - managePopupMenu.getContent().addAll( - new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> WorldManageUIUtils.export(world, sessionLockChannel), managePopup), - new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> WorldManageUIUtils.delete(world, () -> fireEvent(new PageCloseEvent()), sessionLockChannel), managePopup) - ); - toolbar.addNavigationDrawerItem(i18n("world.chunkbase"), SVG.EXPLORE, null, chunkBaseMenuItem -> chunkBaseMenuItem.setOnAction(e -> chunkBasePopup.show(chunkBaseMenuItem, JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, chunkBaseMenuItem.getWidth(), 0))); - - toolbar.addNavigationDrawerItem("管理", SVG.MENU, null, managePopupMenuItem -> - managePopupMenuItem.setOnAction(e -> - managePopup.show(managePopupMenuItem, - JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, - managePopupMenuItem.getWidth(), 0))); } toolbar.addNavigationDrawerItem(i18n("settings.game.exploration"), SVG.FOLDER_OPEN, () -> FXUtils.openFolder(world.getFile()), null); + PopupMenu managePopupMenu = new PopupMenu(); + JFXPopup managePopup = new JFXPopup(managePopupMenu); + + managePopupMenu.getContent().addAll( + new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> WorldManageUIUtils.export(world, sessionLockChannel), managePopup), + new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> WorldManageUIUtils.delete(world, () -> fireEvent(new PageCloseEvent()), sessionLockChannel), managePopup) + ); + + toolbar.addNavigationDrawerItem("管理", SVG.MENU, null, managePopupMenuItem -> + managePopupMenuItem.setOnAction(e -> + managePopup.show(managePopupMenuItem, + JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, + managePopupMenuItem.getWidth(), 0))); + BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0)); left.setBottom(toolbar); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java index 8325c921dc..284d530b00 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java @@ -16,7 +16,6 @@ import java.nio.file.Path; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; public final class WorldManageUIUtils { private WorldManageUIUtils() { @@ -72,7 +71,6 @@ private static void closeSessionLockChannel(World world, FileChannel sessionLock if (sessionLockChannel != null) { try { sessionLockChannel.close(); - LOG.info("Releases the lock on world " + world.getFileName()); } catch (IOException e) { throw new IOException("Failed to close session lock channel", e); } From 81fca701006220d224b5ef516145eb23a5a34697 Mon Sep 17 00:00:00 2001 From: mine_ Date: Fri, 21 Nov 2025 20:28:42 +0800 Subject: [PATCH 19/66] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0i18n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/versions/WorldInfoPage.java | 15 +++++++++------ .../main/resources/assets/lang/I18N.properties | 3 +++ .../main/resources/assets/lang/I18N_zh.properties | 3 +++ .../resources/assets/lang/I18N_zh_CN.properties | 3 +++ 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index c23fc2c7e9..dc5a843f57 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -154,16 +154,19 @@ private void updateControls() { FXUtils.limitSize(iconImageView, 32, 32); iconImageView.setImage(world.getIcon() == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : world.getIcon()); + iconImageView.setCursor(Cursor.HAND); Node editIcon = SVG.EDIT.createIcon(Theme.blackFill(), 12); editIcon.setDisable(worldManagePage.isReadOnly()); editIcon.setCursor(Cursor.HAND); - FXUtils.onClicked(editIcon, () -> Controllers.confirm( - "你需要提供一个分辨率为64×64,格式为PNG的图片,如果不是,HMCL将会将图片进行裁切并将分辨率修改为64×64", "更改世界图标", MessageDialogPane.MessageType.INFO, + Runnable onClickAction = () -> Controllers.confirm( + i18n("world.icon.change.tip"), i18n("world.icon.change"), MessageDialogPane.MessageType.INFO, this::changeWorldIcon, null - )); - FXUtils.installFastTooltip(editIcon, "更改世界图标"); + ); + FXUtils.onClicked(editIcon, onClickAction); + FXUtils.onClicked(iconImageView, onClickAction); + FXUtils.installFastTooltip(editIcon, i18n("world.icon.change")); HBox hBox = new HBox(8); hBox.setAlignment(Pos.CENTER_LEFT); @@ -666,8 +669,8 @@ public String toString() { private void changeWorldIcon() { FileChooser fileChooser = new FileChooser(); - fileChooser.setTitle("选择图像"); - fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Image Files", "*.png")); + fileChooser.setTitle(i18n("world.icon.choose.title")); + fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.png"), "*.png")); fileChooser.setInitialFileName("icon.png"); File file = fileChooser.showOpenDialog(Controllers.getStage()); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index b731111c4f..d239e7aa28 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1140,6 +1140,9 @@ world.export.wizard=Export World "%s" world.extension=World Archive world.game_version=Game Version world.icon=World Icon +world.icon.change=Change world icon +world.icon.change.tip=Please provide a 64x64 PNG image. If the provided image does not meet the requirements, HMCL will automatically crop and resize it to 64x64. +world.icon.choose.title=Select world icon world.import.already_exists=This world already exists. world.import.choose=Choose world archive you want to import world.import.failed=Failed to import this world: %s diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 5d419ba2a2..178517ccd3 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -936,6 +936,9 @@ world.export.location=儲存到 world.export.wizard=匯出世界「%s」 world.extension=存檔壓縮檔 world.icon=世界圖示 +world.icon.change=修改世界圖示 +world.icon.change.tip=請提供一張解析度為 64×64、格式為 PNG 的圖片。若提供的圖片不符規格,HMCL 將會自動裁切並調整為 64×64 的解析度。 +world.icon.choose.title=選擇世界圖示 world.import.already_exists=此世界已經存在 world.import.choose=選取要匯入的存檔壓縮檔 world.import.failed=無法匯入此世界: %s diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 3114308ea2..3242181170 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -947,6 +947,9 @@ world.export.wizard=导出世界“%s” world.extension=世界压缩包 world.game_version=游戏版本 world.icon=世界图标 +world.icon.change=修改世界图标 +world.icon.change.tip=请提供一张分辨率为64×64、格式为PNG的图片。如果提供的图片不符合要求,HMCL将会自动裁剪并调整为64×64的分辨率。 +world.icon.choose.title=选择世界图标 world.import.already_exists=此世界已经存在 world.import.choose=选择要导入的存档压缩包 world.import.failed=无法导入此世界:%s From 0ae56a610bea4aa9a6d4351f6961535e74905e57 Mon Sep 17 00:00:00 2001 From: mine_ Date: Fri, 21 Nov 2025 20:42:45 +0800 Subject: [PATCH 20/66] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/WorldManagePage.java | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java index 2a2fe260d6..d5b3735089 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java @@ -115,23 +115,27 @@ public WorldManagePage(World world, Path backupsDir) { JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, chunkBaseMenuItem.getWidth(), 0))); } + toolbar.addNavigationDrawerItem(i18n("settings.game.exploration"), SVG.FOLDER_OPEN, () -> FXUtils.openFolder(world.getFile()), null); - PopupMenu managePopupMenu = new PopupMenu(); - JFXPopup managePopup = new JFXPopup(managePopupMenu); + { + PopupMenu managePopupMenu = new PopupMenu(); + JFXPopup managePopup = new JFXPopup(managePopupMenu); - managePopupMenu.getContent().addAll( - new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> WorldManageUIUtils.export(world, sessionLockChannel), managePopup), - new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> WorldManageUIUtils.delete(world, () -> fireEvent(new PageCloseEvent()), sessionLockChannel), managePopup) - ); + managePopupMenu.getContent().addAll( + new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> WorldManageUIUtils.export(world, sessionLockChannel), managePopup), + new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> WorldManageUIUtils.delete(world, () -> fireEvent(new PageCloseEvent()), sessionLockChannel), managePopup) + ); - toolbar.addNavigationDrawerItem("管理", SVG.MENU, null, managePopupMenuItem -> - managePopupMenuItem.setOnAction(e -> - managePopup.show(managePopupMenuItem, - JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, - managePopupMenuItem.getWidth(), 0))); + toolbar.addNavigationDrawerItem("管理", SVG.MENU, null, managePopupMenuItem -> + managePopupMenuItem.setOnAction(e -> + managePopup.show(managePopupMenuItem, + JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, + managePopupMenuItem.getWidth(), 0))); + + BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0)); + } - BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0)); left.setBottom(toolbar); // Does it need to be done in the background? From 836bbed3cc8910c46a915498a38fc30436c66508 Mon Sep 17 00:00:00 2001 From: mine_ Date: Sat, 22 Nov 2025 11:05:22 +0800 Subject: [PATCH 21/66] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/WorldInfoPage.java | 32 +++++++++++-------- .../hmcl/ui/versions/WorldManagePage.java | 2 +- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index dc5a843f57..fd9fab923a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -47,6 +47,7 @@ import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.*; +import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.jetbrains.annotations.PropertyKey; import java.io.File; @@ -217,7 +218,6 @@ private void updateControls() { { setLeftLabel(lastPlayedPane, "world.info.last_played"); Label lastPlayedLabel = new Label(); - //lastPlayedLabel.setText(formatDateTime(Instant.ofEpochMilli(world.getLastPlayed()))); setRightTextLabel(lastPlayedPane, lastPlayedLabel, () -> formatDateTime(Instant.ofEpochMilli(world.getLastPlayed()))); } @@ -383,22 +383,29 @@ private void updateControls() { Label spawnLabel = new Label(); setRightTextLabel(spawnPane, spawnLabel, () -> { - Dimension dim = Dimension.of(player.get("SpawnDimension")); - if (dim != null) { //before 25w07a - Tag x = player.get("SpawnX"); - Tag y = player.get("SpawnY"); - Tag z = player.get("SpawnZ"); - - if (x instanceof IntTag intX && y instanceof IntTag intY && z instanceof IntTag intZ) - return dim.formatPosition(intX.getValue(), intY.getValue(), intZ.getValue()); - } else { //after 25w07a + Dimension dimension; + if (GameVersionNumber.asGameVersion(world.getGameVersion()).compareTo("25w07a") >= 0) { CompoundTag respawnTag = player.get("respawn"); - dim = Dimension.of(respawnTag.get("dimension")); + if (respawnTag == null) { + return ""; + } + dimension = Dimension.of(respawnTag.get("dimension")); Tag posTag = respawnTag.get("pos"); if (posTag instanceof IntArrayTag intArrayTag) { - return dim.formatPosition(intArrayTag.getValue(0), intArrayTag.getValue(1), intArrayTag.getValue(2)); + return dimension.formatPosition(intArrayTag.getValue(0), intArrayTag.getValue(1), intArrayTag.getValue(2)); + } + } else { + dimension = Dimension.of(player.get("SpawnDimension")); + if (dimension == null) { + return ""; } + Tag x = player.get("SpawnX"); + Tag y = player.get("SpawnY"); + Tag z = player.get("SpawnZ"); + + if (x instanceof IntTag intX && y instanceof IntTag intY && z instanceof IntTag intZ) + return dimension.formatPosition(intX.getValue(), intY.getValue(), intZ.getValue()); } return ""; }); @@ -677,7 +684,6 @@ private void changeWorldIcon() { if (file == null) return; Image original = new Image(file.toURI().toString()); - Image squareImage = cropCenterSquare(original); Image finalImage; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java index d5b3735089..930ba283b2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java @@ -133,9 +133,9 @@ public WorldManagePage(World world, Path backupsDir) { JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, managePopupMenuItem.getWidth(), 0))); - BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0)); } + BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0)); left.setBottom(toolbar); // Does it need to be done in the background? From ae070c7bbfadca7c1d9f822592463c8b6264b229 Mon Sep 17 00:00:00 2001 From: mine_ Date: Sat, 22 Nov 2025 11:28:18 +0800 Subject: [PATCH 22/66] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index fd9fab923a..2f24075c7a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -649,7 +649,7 @@ private enum Difficulty { static final ObservableList items = FXCollections.observableList(Arrays.asList(values())); static Difficulty of(int d) { - return d >= 0 && d <= items.size() ? items.get(d) : null; + return (d >= 0 && d < items.size()) ? items.get(d) : null; } @Override From 345f2d6cec1ae41dada50e80917d8cd4dc96ce24 Mon Sep 17 00:00:00 2001 From: mine_ Date: Sat, 22 Nov 2025 14:33:52 +0800 Subject: [PATCH 23/66] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=A4=8D?= =?UTF-8?q?=E5=88=B6=E4=B8=96=E7=95=8C=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/WorldListItem.java | 4 + .../hmcl/ui/versions/WorldListItemSkin.java | 1 + .../hmcl/ui/versions/WorldManagePage.java | 5 +- .../hmcl/ui/versions/WorldManageUIUtils.java | 44 +++++++++++ .../resources/assets/lang/I18N.properties | 74 ++----------------- .../resources/assets/lang/I18N_es.properties | 3 + .../resources/assets/lang/I18N_ru.properties | 3 + .../resources/assets/lang/I18N_uk.properties | 3 + .../resources/assets/lang/I18N_zh.properties | 7 ++ .../assets/lang/I18N_zh_CN.properties | 7 ++ .../java/org/jackhuang/hmcl/game/World.java | 10 +++ 11 files changed, 92 insertions(+), 69 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java index 911e66427f..7e0050baaf 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java @@ -53,6 +53,10 @@ public void delete() { WorldManageUIUtils.delete(world, () -> parent.remove(this)); } + public void copy() { + WorldManageUIUtils.copyWorld(world, parent::refresh); + } + public void reveal() { FXUtils.openFolder(world.getFile()); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java index 89b8006b57..6dcd394595 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java @@ -137,6 +137,7 @@ public void showPopupMenu(JFXPopup.PopupHPosition hPosition, double initOffsetX, new MenuSeparator(), new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), item::export, popup), new IconedMenuItem(SVG.DELETE, i18n("world.delete"), item::delete, popup), + new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.copy"), item::copy, popup), new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.world"), item::reveal, popup)); JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java index 930ba283b2..e498b14e9f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java @@ -124,10 +124,11 @@ public WorldManagePage(World world, Path backupsDir) { managePopupMenu.getContent().addAll( new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> WorldManageUIUtils.export(world, sessionLockChannel), managePopup), - new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> WorldManageUIUtils.delete(world, () -> fireEvent(new PageCloseEvent()), sessionLockChannel), managePopup) + new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> WorldManageUIUtils.delete(world, () -> fireEvent(new PageCloseEvent()), sessionLockChannel), managePopup), + new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.copy"), () -> WorldManageUIUtils.copyWorld(world, null, sessionLockChannel), managePopup) ); - toolbar.addNavigationDrawerItem("管理", SVG.MENU, null, managePopupMenuItem -> + toolbar.addNavigationDrawerItem(i18n("settings.game.management"), SVG.MENU, null, managePopupMenuItem -> managePopupMenuItem.setOnAction(e -> managePopup.show(managePopupMenuItem, JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java index 284d530b00..a87f6c716f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java @@ -6,6 +6,7 @@ import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.Controllers; +import org.jackhuang.hmcl.ui.construct.InputDialogPane; import org.jackhuang.hmcl.ui.construct.MessageDialogPane; import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider; import org.jackhuang.hmcl.util.StringUtils; @@ -13,9 +14,11 @@ import java.io.IOException; import java.nio.channels.FileChannel; +import java.nio.file.Files; import java.nio.file.Path; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; public final class WorldManageUIUtils { private WorldManageUIUtils() { @@ -67,6 +70,47 @@ public static void export(World world, FileChannel sessionLockChannel) { Controllers.getDecorator().startWizard(new SinglePageWizardProvider(controller -> new WorldExportPage(world, file, controller::onFinish))); } + public static void copyWorld(World world, Runnable runnable) { + copyWorld(world, runnable, null); + } + + public static void copyWorld(World world, Runnable runnable, FileChannel sessionLockChannel) { + Path worldPath = world.getFile(); + Controllers.dialog(new InputDialogPane( + i18n("world.copy.prompt"), + "", + (result, resolve, reject) -> { + if (StringUtils.isBlank(result)) { + reject.accept(i18n("world.copy.failed.empty_name")); + return; + } + + if (result.contains("/") || result.contains("\\") || !FileUtils.isNameValid(result)) { + reject.accept(i18n("world.copy.failed.invalid_name")); + return; + } + + Path targetDir = worldPath.resolveSibling(result); + if (Files.exists(targetDir)) { + reject.accept(i18n("world.copy.failed.already_exists")); + return; + } + + try { + closeSessionLockChannel(world, sessionLockChannel); + world.copy(result); + Controllers.showToast(i18n("world.copy.success.toast")); + if (runnable != null) { + runnable.run(); + } + resolve.run(); + } catch (IOException e) { + LOG.warning("Failed to copy world", e); + reject.accept(i18n("world.copy.failed")); + } + })); + } + private static void closeSessionLockChannel(World world, FileChannel sessionLockChannel) throws IOException { if (sessionLockChannel != null) { try { diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index d239e7aa28..8b4bb7375f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -15,10 +15,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # - # Contributors: dxNeil, machinesmith42 # and Byacrya for basically retranslating it - about=About about.copyright=Copyright about.copyright.statement=Copyright © 2025 huangyuhui @@ -50,7 +48,6 @@ about.thanks_to.users.statement=Thanks for donations, bug reports, and so on. about.thanks_to.yushijinhun.statement=For providing the authlib-injector related support. about.open_source=Open Source about.open_source.statement=GPL v3 (https://github.com/HMCL-dev/HMCL) - account=Accounts account.cape=Cape account.character=Player @@ -164,16 +161,13 @@ account.skin.upload=Upload/Edit Skin account.skin.upload.failed=Failed to upload skin. account.skin.invalid_skin=Invalid skin file. account.username=Username - archive.author=Author(s) archive.date=Publish Date archive.file.name=File Name archive.version=Version - assets.download=Downloading Assets assets.download_all=Validating Assets Integrity assets.index.malformed=Index files of downloaded assets are corrupted. You can resolve this problem by clicking "Manage → Update Game Assets" on the "Edit Instance" page. - button.cancel=Cancel button.change_source=Change Download Source button.clear=Clear @@ -196,17 +190,12 @@ button.save_as=Save As button.select_all=Select All button.view=View button.yes=Yes - chat=Join Group Chat - color.recent=Recommended color.custom=Custom Color - crash.NoClassDefFound=Please verify the integrity of this software, or try updating your Java. crash.user_fault=The launcher crashed due to corrupted Java or system environment. Please make sure your Java or OS is installed properly. - curse.category.0=All - # https://addons-ecs.forgesvc.net/api/v2/category/section/4471 curse.category.4474=Sci-Fi curse.category.4481=Small / Light @@ -225,7 +214,6 @@ curse.category.4472=Tech curse.category.4473=Magic curse.category.5128=Vanilla+ curse.category.7418=Horror - # https://addons-ecs.forgesvc.net/api/v2/category/section/6 curse.category.5299=Education curse.category.5232=Galacticraft @@ -282,7 +270,6 @@ curse.category.4558=Redstone curse.category.4843=Automation curse.category.4906=MCreator curse.category.7669=Twilight Forest - # https://addons-ecs.forgesvc.net/api/v2/category/section/12 curse.category.5244=Font Packs curse.category.5193=Data Packs @@ -300,7 +287,6 @@ curse.category.404=Animated curse.category.4465=Mod Support curse.category.402=Medieval curse.category.401=Modern - # https://addons-ecs.forgesvc.net/api/v2/category/section/17 curse.category.4464=Modded World curse.category.250=Game Map @@ -309,7 +295,6 @@ curse.category.251=Parkour curse.category.253=Survival curse.category.248=Adventure curse.category.252=Puzzle - # https://addons-ecs.forgesvc.net/api/v2/category/section/4546 curse.category.4551=Hardcore Questing Mode curse.category.4548=Lucky Blocks @@ -322,16 +307,13 @@ curse.category.4547=Configuration curse.category.4550=Quests curse.category.4555=World Gen curse.category.4552=Scripts - curse.sort.author=Author curse.sort.date_created=Date Created curse.sort.last_updated=Last Updated curse.sort.name=Name curse.sort.popularity=Popularity curse.sort.total_downloads=Total Downloads - datetime.format=MMM d, yyyy, h\:mm\:ss a - download=Download download.hint=Install games and modpacks or download mods, resource packs, shaders, and worlds. download.code.404=File "%s" not found on the remote server. @@ -363,20 +345,17 @@ download.javafx.prepare=Preparing to download download.speed.byte_per_second=%d B/s download.speed.kibibyte_per_second=%.1f KiB/s download.speed.megabyte_per_second=%.1f MiB/s - exception.access_denied=HMCL is unable to access the file "%s". It may be locked by another process.\n\ \n\ For Windows users, you can open "Resource Monitor" to check if another process is currently using it. If so, you can try again after terminating that process.\n\ If not, please check if your user account has adequate permissions to access it. exception.artifact_malformed=Cannot verify the integrity of the downloaded files. exception.ssl_handshake=Failed to establish SSL connection because the SSL certificate is missing from the current Java installation. You can try opening HMCL with another Java installation and try again. - extension.bat=Windows Batch File extension.mod=Mod File extension.png=Image File extension.ps1=Windows PowerShell Script extension.sh=Shell Script - fatal.create_hmcl_current_directory_failure=Hello Minecraft! Launcher cannot create the HMCL directory (%s). Please move HMCL to another location and reopen it. fatal.javafx.incompatible=Missing JavaFX environment.\n\ Hello Minecraft! Launcher cannot automatically install JavaFX on Java <11.\n\ @@ -431,7 +410,6 @@ fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher has provided \n\ If you are using the Qualcomm platform, you may need to install the OpenGL Compatibility Pack before playing games.\n\ Click the link to navigate to the Microsoft Store and install the compatibility pack. - feedback=Feedback feedback.channel=Feedback Channel feedback.discord=Discord @@ -440,9 +418,7 @@ feedback.github=GitHub Issues feedback.github.statement=Submit an issue on GitHub. feedback.qq_group=HMCL User QQ Group feedback.qq_group.statement=Welcome to join our user QQ group. - file=File - folder.config=Configs folder.game=Working Directory folder.logs=Logs @@ -453,7 +429,6 @@ folder.saves=Saves folder.schematics=Schematics folder.screenshots=Screenshots folder.world=World Directory - game=Games game.crash.feedback=Please do not share screenshots or photos of this interface with others! If you ask for help from others, please click "Export Crash Logs" and send the exported file to others for analysis. game.crash.info=Crash Info @@ -684,16 +659,13 @@ game.crash.reason.unsatisfied_link_error=Failed to launch Minecraft because of m game.crash.title=Game Crashed game.directory=Game Path game.version=Game Instance - help=Help help.doc=Hello Minecraft! Launcher Documentation help.detail=For datapack and modpack makers. - input.email=The username must be an email address. input.number=The input must be numbers. input.not_empty=This is a required field. input.url=The input must be a valid URL. - install=New Instance install.change_version=Change Version install.change_version.confirm=Are you sure you want to switch %1$s from version %2$s to %3$s? @@ -743,7 +715,6 @@ install.new_game.installation=Instance Installation install.new_game.malformed=Invalid name. install.select=Choose operation install.success=Successfully installed. - java.add=Add Java java.add.failed=This Java is invalid or incompatible with the current platform. java.disable=Disable Java @@ -774,9 +745,7 @@ java.install.warning.invalid_character=Illegal character in name java.installing=Installing Java java.uninstall=Uninstall Java java.uninstall.confirm=Are you sure you want to uninstall this Java? This action cannot be undone! - lang.default=Use System Locales - launch.advice=%s Do you still want to continue to launch? launch.advice.multi=The following problems were detected:\n\n%s\n\nThese problems may prevent you from launching the game or affect gaming experience.\nDo you still want to continue to launch? launch.advice.java.auto=The current Java version is not compatible with the instance.\n\nClick "Yes" to automatically choose the most compatible Java version. Or, you can navigate to "Global/Instance-specific Settings → Java" to choose one yourself. @@ -821,7 +790,6 @@ launch.state.logging_in=Logging in launch.state.modpack=Downloading required files launch.state.waiting_launching=Waiting for the game to launch launch.invalid_java=Invalid Java path. Please reset the Java path. - launcher=Launcher launcher.agreement=ToS and EULA launcher.agreement.accept=Accept @@ -845,12 +813,9 @@ launcher.crash=Hello Minecraft! Launcher has encountered a fatal error! Please c launcher.crash.java_internal_error=Hello Minecraft! Launcher has encountered a fatal error because your Java is corrupted. Please uninstall your Java and download a suitable Java here. launcher.crash.hmcl_out_dated=Hello Minecraft! Launcher has encountered a fatal error! Your launcher is outdated. Please update your launcher! launcher.update_java=Please update your Java version. - libraries.download=Downloading Libraries - login.empty_username=You have not set your username yet! login.enter_password=Please enter your password. - logwindow.show_lines=Show Row Number logwindow.terminate_game=Kill Game Process logwindow.title=Log @@ -859,9 +824,7 @@ logwindow.autoscroll=Auto-scroll logwindow.export_game_crash_logs=Export Crash Logs logwindow.export_dump=Export Game Stack Dump logwindow.export_dump.no_dependency=Your Java does not contain the dependencies to create the stack dump. Please join our Discord or QQ group for help. - main_page=Home - message.cancelled=Operation Canceled message.confirm=Confirm message.copied=Copied to clipboard @@ -874,7 +837,6 @@ message.info=Information message.success=Operation successfully completed message.unknown=Unknown message.warning=Warning - modpack=Modpacks modpack.choose=Choose Modpack modpack.choose.local=Import from Local File @@ -953,7 +915,6 @@ modpack.wizard.step.initialization.warning=Before making a modpack, please make \n\ Keep in mind that you are not allowed to add mods and resource packs that explicitly state they could not to be distributed or put in a modpack. modpack.wizard.step.initialization.server=Click here for more information on how to make a server modpack that can be automatically updated. - modrinth.category.adventure=Adventure modrinth.category.atmosphere=Atmosphere modrinth.category.audio=Audio @@ -1048,7 +1009,6 @@ modrinth.category.64x=64x modrinth.category.128x=128x modrinth.category.256x=256x modrinth.category.512x+=512x+ - mods=Mods mods.add=Add Mod mods.add.failed=Failed to add mod %s. @@ -1098,22 +1058,18 @@ mods.warning.loader_mismatch=Mod loader mismatch mods.install=Install mods.save_as=Save As mods.unknown=Unknown Mod - nbt.entries=%s entries nbt.open.failed=Failed to open file nbt.save.failed=Failed to save file nbt.title=View File - %s - datapack=Datapacks datapack.add=Install Datapack datapack.choose_datapack=Choose datapack to import datapack.extension=Datapack datapack.title=World [%s] - Datapacks - web.failed=Failed to load page web.open_in_browser=Do you want to open this address in a browser:\n%s web.view_in_browser=View all in browser - world=Worlds world.add=Add World world.backup=World Backup @@ -1127,6 +1083,13 @@ world.chunkbase.end_city=End City world.chunkbase.seed_map=Seed Map world.chunkbase.stronghold=Stronghold world.chunkbase.nether_fortress=Nether Fortress +world.copy=Copy the World +world.copy.prompt=Please enter the name of the copied world +world.copy.failed.already_exists=Directory already exists +world.copy.failed.empty_name=Name cannot be empty +world.copy.failed.invalid_name=Name contains invalid characters +world.copy.failed=Copy world failed +world.copy.success.toast=Copy world success world.datapack=Datapacks world.datetime=Last played on %s world.delete=Delete the World @@ -1186,7 +1149,6 @@ world.manage.title=World - %s world.name=World Name world.name.enter=Enter the world name world.show_all=Show All - profile=Game Directories profile.already_exists=This name already exists. Please use a different name. profile.default=Current @@ -1199,7 +1161,6 @@ profile.new=New Directory profile.title=Game Directories profile.selected=Selected profile.use_relative_path=Use a relative path for the game directory if possible - repositories.custom=Custom Maven Repository (%s) repositories.maven_central=Universal (Maven Central) repositories.tencentcloud_mirror=Chinese Mainland Mirror (Tencent Cloud Maven Repository) @@ -1209,12 +1170,9 @@ repositories.chooser=HMCL requires JavaFX to work.\n\ \n\ Repositories: repositories.chooser.title=Choose download source for JavaFX - resourcepack=Resource Packs resourcepack.download.title=Download Resource Pack - %1s - reveal.in_file_manager=Reveal in File Manager - schematics=Schematics schematics.add=Add Schematic Files schematics.add.failed=Failed to add schematic files @@ -1237,7 +1195,6 @@ schematics.info.total_volume=Total Volume schematics.info.version=Schematic Version schematics.manage=Schematics schematics.sub_items=%d sub-item(s) - search=Search search.hint.chinese=Search in English and Chinese search.hint.english=Search in English only @@ -1248,13 +1205,10 @@ search.previous_page=Previous search.next_page=Next search.last_page=Last search.page_n=%1$d / %2$s - selector.choose=Choose selector.choose_file=Choose file selector.custom=Custom - settings=Settings - settings.advanced=Advanced Settings settings.advanced.modify=Edit Advanced Settings settings.advanced.title=Advanced Settings - %s @@ -1327,9 +1281,7 @@ settings.advanced.workaround=Workaround settings.advanced.workaround.warning=Workaround options are intended only for advanced users. Tweaking with these options may crash the game. Unless you know what you are doing, please do not edit these options. settings.advanced.wrapper_launcher=Wrapper Command settings.advanced.wrapper_launcher.prompt=Allows launching using an extra wrapper program like "optirun" on Linux - settings.custom=Custom - settings.game=Settings settings.game.copy_global=Copy from Global Settings settings.game.copy_global.copy_all=Copy All @@ -1352,9 +1304,7 @@ settings.game.working_directory.choose=Choose the working directory settings.game.working_directory.hint=Enable the "Isolated" option in "Working Directory" to allow the current instance to store its settings, saves, and mods in a separate directory.\n\ \n\ It is recommended to enable this option to avoid mod conflicts, but you will need to move your saves manually. - settings.icon=Icon - settings.launcher=Launcher Settings settings.launcher.appearance=Appearance settings.launcher.common_path.tooltip=HMCL will put all game assets and dependencies here. If there are existing libraries in the game directory, then HMCL will prefer to use them first. @@ -1395,7 +1345,6 @@ settings.launcher.title_transparent=Transparent Titlebar settings.launcher.turn_off_animations=Disable Animation (Applies After Restart) settings.launcher.version_list_source=Version List settings.launcher.background.settings.opacity=Opacity - settings.memory=Memory settings.memory.allocate.auto=%1$.1f GiB Minimum / %2$.1f GiB Allocated settings.memory.allocate.auto.exceeded=%1$.1f GiB Minimum / %2$.1f GiB Allocated (%3$.1f GiB Available) @@ -1416,14 +1365,11 @@ settings.type.global.edit=Edit Global Settings settings.type.special.enable=Enable Instance-specific Settings settings.type.special.edit=Edit Current Instance Settings settings.type.special.edit.hint=Current instance "%s" has enabled the "Instance-specific Settings". All options on this page will NOT affect that instance. Click here to edit its own settings. - sponsor=Donors sponsor.bmclapi=Downloads for the Chinese Mainland are provided by BMCLAPI. Click here for more information. sponsor.hmcl=Hello Minecraft! Launcher is a FOSS Minecraft launcher that allows users to manage multiple Minecraft instances easily. Click here for more information. - system.architecture=Architecture system.operating_system=Operating System - terracotta=Multiplayer terracotta.easytier=About EasyTier terracotta.terracotta=Terracotta | Multiplayer @@ -1499,9 +1445,7 @@ terracotta.unsupported=Multiplayer is not yet supported on the current platform. terracotta.unsupported.os.windows.old=Multiplayer requires Windows 10 or later. Please update your system. terracotta.unsupported.arch.32bit=Multiplayer is not supported on 32-bit systems. Please upgrade to a 64-bit system. terracotta.unsupported.arch.loongarch64_ow=Multiplayer is not supported on Linux LoongArch64 Old World distributions. Please update to a New World distribution (such as AOSC OS). - unofficial.hint=You are using an unofficial build of HMCL. We cannot guarantee its security. - update=Update update.accept=Update update.changelog=Changelog @@ -1532,7 +1476,6 @@ update.no_browser=Cannot open in system browser. But we copied the link to your update.tooltip=Update update.preview=Preview HMCL releases early update.preview.tooltip=Enable this option to receive new versions of HMCL early for testing before their official release. - version=Games version.name=Instance Name version.cannot_read=Failed to parse the game instance, installation cannot continue. @@ -1584,16 +1527,13 @@ version.search=Name version.search.prompt=Enter the version name to search version.settings=Settings version.update=Update Modpack - warning.java_interpreted_mode=HMCL is running in an interpreted Java environment, which will greatly affect performance.\n\ \n\ We recommend using a Java with JIT support to open HMCL for the best experience. warning.software_rendering=HMCL is currently using software rendering, which will greatly affect performance. - wiki.tooltip=Minecraft Wiki Page wiki.version.game=https://minecraft.wiki/w/Java_Edition_%s wiki.version.game.snapshot=https://minecraft.wiki/w/Java_Edition_%s - wizard.prev=< Prev wizard.failed=Failed wizard.finish=Finish diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index f4e0bc6d41..47fcbd9373 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -1128,6 +1128,9 @@ world.chunkbase.end_city=Ciudad del End world.chunkbase.seed_map=Vista previa de la generación mundial world.chunkbase.stronghold=Fortaleza world.chunkbase.nether_fortress=Fortaleza del Nether +world.copy.failed.already_exists=El directorio ya existe +world.copy.failed.empty_name=El nombre no puede estar vacío +world.copy.failed.invalid_name=El nombre contiene caracteres no válidos world.datapack=Paquetes de datos world.datetime=Jugado por última vez en %s world.delete=Eliminar este mundo diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index e83949930f..b5ba3da75c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -1123,6 +1123,9 @@ world.chunkbase.end_city=Город Края world.chunkbase.seed_map=Предпросмотр генерации мира world.chunkbase.stronghold=Крепость world.chunkbase.nether_fortress=Крепость Нижнего мира +world.copy.failed.already_exists=Папка уже существует +world.copy.failed.empty_name=Название не может быть пустым +world.copy.failed.invalid_name=Название содержит недопустимые символы world.delete=Удалить этот мир world.delete.failed=Не удалось удалить мир.\n%s world.datapack=Наборы данных diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index e2684eb773..cf0887bbac 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -1065,6 +1065,9 @@ world.chunkbase.end_city=Кінцеве місто world.chunkbase.seed_map=Карта насіння world.chunkbase.stronghold=Фортеця world.chunkbase.nether_fortress=Форт Незеру +world.copy.failed.already_exists=Каталог вже існує +world.copy.failed.empty_name=Назва не може бути порожньою +world.copy.failed.invalid_name=Назва містить недійсні символи world.datapack=Datapacks world.datetime=Останній раз грали %s world.delete=Видалити цей світ diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 178517ccd3..7645a05b1e 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -924,6 +924,13 @@ world.chunkbase.end_city=終界城地圖 world.chunkbase.seed_map=種子地圖 world.chunkbase.stronghold=要塞地圖 world.chunkbase.nether_fortress=地獄要塞地圖 +world.copy=複製此世界 +world.copy.prompt=輸入複製後的世界名稱 +world.copy.failed.already_exists=目錄已存在 +world.copy.failed.empty_name=名稱不能為空 +world.copy.failed.invalid_name=名稱中包含無效字元 +world.copy.failed=複製世界失敗 +world.copy.success.toast=複製世界成功 world.datapack=資料包管理 world.datetime=上一次遊戲時間: %s world.delete=刪除此世界 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 3242181170..f6c8b07335 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -934,6 +934,13 @@ world.chunkbase.end_city=末地城地图 world.chunkbase.seed_map=种子地图 world.chunkbase.stronghold=要塞地图 world.chunkbase.nether_fortress=下界要塞地图 +world.copy=复制此世界 +world.copy.prompt=输入复制后的世界名称 +world.copy.failed.already_exists=文件夹已存在 +world.copy.failed.empty_name=名称不能为空 +world.copy.failed.invalid_name=名称中包含非法字符 +world.copy.failed=复制世界失败 +world.copy.success.toast=复制世界成功 world.datapack=数据包管理 world.datetime=上一次游戏时间: %s world.delete=删除此世界 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java index 6d99145fdd..e3597371e6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java @@ -286,6 +286,16 @@ public void delete() throws IOException { FileUtils.forceDelete(file); } + public void copy(String newName) throws IOException { + if (!Files.isDirectory(file)) + throw new IOException(); + + Path newPath = file.resolveSibling(newName); + FileUtils.copyDirectory(file, newPath); + World newWorld = new World(newPath); + newWorld.rename(newName); + } + public CompoundTag readLevelDat() throws IOException { if (!Files.isDirectory(file)) throw new IOException("Not a valid world directory"); From af0001152c36d742ded2424b2156413f4ac62794 Mon Sep 17 00:00:00 2001 From: mine_ Date: Sat, 22 Nov 2025 15:26:06 +0800 Subject: [PATCH 24/66] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/WorldManageUIUtils.java | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java index a87f6c716f..220b335d44 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java @@ -98,16 +98,26 @@ public static void copyWorld(World world, Runnable runnable, FileChannel session try { closeSessionLockChannel(world, sessionLockChannel); - world.copy(result); - Controllers.showToast(i18n("world.copy.success.toast")); - if (runnable != null) { - runnable.run(); - } - resolve.run(); } catch (IOException e) { - LOG.warning("Failed to copy world", e); - reject.accept(i18n("world.copy.failed")); + Controllers.dialog(i18n("world.locked.failed"), null, MessageDialogPane.MessageType.WARNING); + LOG.warning("unlock world fail", e); } + + Task.runAsync(Schedulers.io(), () -> world.copy(result)) + .thenAcceptAsync(Schedulers.javafx(), (Void) -> Controllers.showToast(i18n("world.copy.success.toast"))) + .thenAcceptAsync(Schedulers.javafx(), (Void) -> { + if (runnable != null) { + runnable.run(); + } + } + ).whenComplete(Schedulers.javafx(), (exception) -> { + if (exception == null) { + resolve.run(); + } else { + reject.accept(i18n("world.copy.failed")); + } + }) + .start(); })); } From 7f97f552c2c5a1a359d35bc0d365b37da727345e Mon Sep 17 00:00:00 2001 From: mine_ Date: Sat, 22 Nov 2025 15:56:19 +0800 Subject: [PATCH 25/66] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=89=88?= =?UTF-8?q?=E6=9D=83=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/WorldManageUIUtils.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java index 220b335d44..e1dbbd04f6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java @@ -1,3 +1,20 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ package org.jackhuang.hmcl.ui.versions; import javafx.stage.FileChooser; From 88a2c6c7656de6e0572385a74893d0e4a69cf00e Mon Sep 17 00:00:00 2001 From: mine_ Date: Sat, 22 Nov 2025 19:56:13 +0800 Subject: [PATCH 26/66] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java index e1dbbd04f6..efb197937f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java @@ -143,7 +143,7 @@ private static void closeSessionLockChannel(World world, FileChannel sessionLock try { sessionLockChannel.close(); } catch (IOException e) { - throw new IOException("Failed to close session lock channel", e); + throw new IOException("Failed to close session lock channel of the world " + world.getFile(), e); } } } From e6ff6c159ac6386c10c924cc5e5725ddbee6bf64 Mon Sep 17 00:00:00 2001 From: mine_ Date: Sun, 23 Nov 2025 09:33:48 +0800 Subject: [PATCH 27/66] =?UTF-8?q?fix:=20=E6=81=A2=E5=A4=8D=E6=84=8F?= =?UTF-8?q?=E5=A4=96=E7=A7=BB=E9=99=A4=E7=9A=84=E7=A9=BA=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/assets/lang/I18N.properties | 69 ++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 8b4bb7375f..c0aef93943 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -15,8 +15,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # + # Contributors: dxNeil, machinesmith42 # and Byacrya for basically retranslating it + about=About about.copyright=Copyright about.copyright.statement=Copyright © 2025 huangyuhui @@ -48,6 +50,7 @@ about.thanks_to.users.statement=Thanks for donations, bug reports, and so on. about.thanks_to.yushijinhun.statement=For providing the authlib-injector related support. about.open_source=Open Source about.open_source.statement=GPL v3 (https://github.com/HMCL-dev/HMCL) + account=Accounts account.cape=Cape account.character=Player @@ -161,13 +164,16 @@ account.skin.upload=Upload/Edit Skin account.skin.upload.failed=Failed to upload skin. account.skin.invalid_skin=Invalid skin file. account.username=Username + archive.author=Author(s) archive.date=Publish Date archive.file.name=File Name archive.version=Version + assets.download=Downloading Assets assets.download_all=Validating Assets Integrity assets.index.malformed=Index files of downloaded assets are corrupted. You can resolve this problem by clicking "Manage → Update Game Assets" on the "Edit Instance" page. + button.cancel=Cancel button.change_source=Change Download Source button.clear=Clear @@ -190,12 +196,17 @@ button.save_as=Save As button.select_all=Select All button.view=View button.yes=Yes + chat=Join Group Chat + color.recent=Recommended color.custom=Custom Color + crash.NoClassDefFound=Please verify the integrity of this software, or try updating your Java. crash.user_fault=The launcher crashed due to corrupted Java or system environment. Please make sure your Java or OS is installed properly. + curse.category.0=All + # https://addons-ecs.forgesvc.net/api/v2/category/section/4471 curse.category.4474=Sci-Fi curse.category.4481=Small / Light @@ -214,6 +225,7 @@ curse.category.4472=Tech curse.category.4473=Magic curse.category.5128=Vanilla+ curse.category.7418=Horror + # https://addons-ecs.forgesvc.net/api/v2/category/section/6 curse.category.5299=Education curse.category.5232=Galacticraft @@ -270,6 +282,7 @@ curse.category.4558=Redstone curse.category.4843=Automation curse.category.4906=MCreator curse.category.7669=Twilight Forest + # https://addons-ecs.forgesvc.net/api/v2/category/section/12 curse.category.5244=Font Packs curse.category.5193=Data Packs @@ -287,6 +300,7 @@ curse.category.404=Animated curse.category.4465=Mod Support curse.category.402=Medieval curse.category.401=Modern + # https://addons-ecs.forgesvc.net/api/v2/category/section/17 curse.category.4464=Modded World curse.category.250=Game Map @@ -295,6 +309,7 @@ curse.category.251=Parkour curse.category.253=Survival curse.category.248=Adventure curse.category.252=Puzzle + # https://addons-ecs.forgesvc.net/api/v2/category/section/4546 curse.category.4551=Hardcore Questing Mode curse.category.4548=Lucky Blocks @@ -307,13 +322,16 @@ curse.category.4547=Configuration curse.category.4550=Quests curse.category.4555=World Gen curse.category.4552=Scripts + curse.sort.author=Author curse.sort.date_created=Date Created curse.sort.last_updated=Last Updated curse.sort.name=Name curse.sort.popularity=Popularity curse.sort.total_downloads=Total Downloads + datetime.format=MMM d, yyyy, h\:mm\:ss a + download=Download download.hint=Install games and modpacks or download mods, resource packs, shaders, and worlds. download.code.404=File "%s" not found on the remote server. @@ -345,17 +363,20 @@ download.javafx.prepare=Preparing to download download.speed.byte_per_second=%d B/s download.speed.kibibyte_per_second=%.1f KiB/s download.speed.megabyte_per_second=%.1f MiB/s + exception.access_denied=HMCL is unable to access the file "%s". It may be locked by another process.\n\ \n\ For Windows users, you can open "Resource Monitor" to check if another process is currently using it. If so, you can try again after terminating that process.\n\ If not, please check if your user account has adequate permissions to access it. exception.artifact_malformed=Cannot verify the integrity of the downloaded files. exception.ssl_handshake=Failed to establish SSL connection because the SSL certificate is missing from the current Java installation. You can try opening HMCL with another Java installation and try again. + extension.bat=Windows Batch File extension.mod=Mod File extension.png=Image File extension.ps1=Windows PowerShell Script extension.sh=Shell Script + fatal.create_hmcl_current_directory_failure=Hello Minecraft! Launcher cannot create the HMCL directory (%s). Please move HMCL to another location and reopen it. fatal.javafx.incompatible=Missing JavaFX environment.\n\ Hello Minecraft! Launcher cannot automatically install JavaFX on Java <11.\n\ @@ -410,6 +431,7 @@ fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher has provided \n\ If you are using the Qualcomm platform, you may need to install the OpenGL Compatibility Pack before playing games.\n\ Click the link to navigate to the Microsoft Store and install the compatibility pack. + feedback=Feedback feedback.channel=Feedback Channel feedback.discord=Discord @@ -418,7 +440,9 @@ feedback.github=GitHub Issues feedback.github.statement=Submit an issue on GitHub. feedback.qq_group=HMCL User QQ Group feedback.qq_group.statement=Welcome to join our user QQ group. + file=File + folder.config=Configs folder.game=Working Directory folder.logs=Logs @@ -429,6 +453,7 @@ folder.saves=Saves folder.schematics=Schematics folder.screenshots=Screenshots folder.world=World Directory + game=Games game.crash.feedback=Please do not share screenshots or photos of this interface with others! If you ask for help from others, please click "Export Crash Logs" and send the exported file to others for analysis. game.crash.info=Crash Info @@ -659,13 +684,16 @@ game.crash.reason.unsatisfied_link_error=Failed to launch Minecraft because of m game.crash.title=Game Crashed game.directory=Game Path game.version=Game Instance + help=Help help.doc=Hello Minecraft! Launcher Documentation help.detail=For datapack and modpack makers. + input.email=The username must be an email address. input.number=The input must be numbers. input.not_empty=This is a required field. input.url=The input must be a valid URL. + install=New Instance install.change_version=Change Version install.change_version.confirm=Are you sure you want to switch %1$s from version %2$s to %3$s? @@ -715,6 +743,7 @@ install.new_game.installation=Instance Installation install.new_game.malformed=Invalid name. install.select=Choose operation install.success=Successfully installed. + java.add=Add Java java.add.failed=This Java is invalid or incompatible with the current platform. java.disable=Disable Java @@ -745,7 +774,9 @@ java.install.warning.invalid_character=Illegal character in name java.installing=Installing Java java.uninstall=Uninstall Java java.uninstall.confirm=Are you sure you want to uninstall this Java? This action cannot be undone! + lang.default=Use System Locales + launch.advice=%s Do you still want to continue to launch? launch.advice.multi=The following problems were detected:\n\n%s\n\nThese problems may prevent you from launching the game or affect gaming experience.\nDo you still want to continue to launch? launch.advice.java.auto=The current Java version is not compatible with the instance.\n\nClick "Yes" to automatically choose the most compatible Java version. Or, you can navigate to "Global/Instance-specific Settings → Java" to choose one yourself. @@ -790,6 +821,7 @@ launch.state.logging_in=Logging in launch.state.modpack=Downloading required files launch.state.waiting_launching=Waiting for the game to launch launch.invalid_java=Invalid Java path. Please reset the Java path. + launcher=Launcher launcher.agreement=ToS and EULA launcher.agreement.accept=Accept @@ -813,9 +845,12 @@ launcher.crash=Hello Minecraft! Launcher has encountered a fatal error! Please c launcher.crash.java_internal_error=Hello Minecraft! Launcher has encountered a fatal error because your Java is corrupted. Please uninstall your Java and download a suitable Java here. launcher.crash.hmcl_out_dated=Hello Minecraft! Launcher has encountered a fatal error! Your launcher is outdated. Please update your launcher! launcher.update_java=Please update your Java version. + libraries.download=Downloading Libraries + login.empty_username=You have not set your username yet! login.enter_password=Please enter your password. + logwindow.show_lines=Show Row Number logwindow.terminate_game=Kill Game Process logwindow.title=Log @@ -824,7 +859,9 @@ logwindow.autoscroll=Auto-scroll logwindow.export_game_crash_logs=Export Crash Logs logwindow.export_dump=Export Game Stack Dump logwindow.export_dump.no_dependency=Your Java does not contain the dependencies to create the stack dump. Please join our Discord or QQ group for help. + main_page=Home + message.cancelled=Operation Canceled message.confirm=Confirm message.copied=Copied to clipboard @@ -837,6 +874,7 @@ message.info=Information message.success=Operation successfully completed message.unknown=Unknown message.warning=Warning + modpack=Modpacks modpack.choose=Choose Modpack modpack.choose.local=Import from Local File @@ -915,6 +953,7 @@ modpack.wizard.step.initialization.warning=Before making a modpack, please make \n\ Keep in mind that you are not allowed to add mods and resource packs that explicitly state they could not to be distributed or put in a modpack. modpack.wizard.step.initialization.server=Click here for more information on how to make a server modpack that can be automatically updated. + modrinth.category.adventure=Adventure modrinth.category.atmosphere=Atmosphere modrinth.category.audio=Audio @@ -1009,6 +1048,7 @@ modrinth.category.64x=64x modrinth.category.128x=128x modrinth.category.256x=256x modrinth.category.512x+=512x+ + mods=Mods mods.add=Add Mod mods.add.failed=Failed to add mod %s. @@ -1058,18 +1098,22 @@ mods.warning.loader_mismatch=Mod loader mismatch mods.install=Install mods.save_as=Save As mods.unknown=Unknown Mod + nbt.entries=%s entries nbt.open.failed=Failed to open file nbt.save.failed=Failed to save file nbt.title=View File - %s + datapack=Datapacks datapack.add=Install Datapack datapack.choose_datapack=Choose datapack to import datapack.extension=Datapack datapack.title=World [%s] - Datapacks + web.failed=Failed to load page web.open_in_browser=Do you want to open this address in a browser:\n%s web.view_in_browser=View all in browser + world=Worlds world.add=Add World world.backup=World Backup @@ -1149,6 +1193,7 @@ world.manage.title=World - %s world.name=World Name world.name.enter=Enter the world name world.show_all=Show All + profile=Game Directories profile.already_exists=This name already exists. Please use a different name. profile.default=Current @@ -1161,6 +1206,7 @@ profile.new=New Directory profile.title=Game Directories profile.selected=Selected profile.use_relative_path=Use a relative path for the game directory if possible + repositories.custom=Custom Maven Repository (%s) repositories.maven_central=Universal (Maven Central) repositories.tencentcloud_mirror=Chinese Mainland Mirror (Tencent Cloud Maven Repository) @@ -1170,9 +1216,12 @@ repositories.chooser=HMCL requires JavaFX to work.\n\ \n\ Repositories: repositories.chooser.title=Choose download source for JavaFX + resourcepack=Resource Packs resourcepack.download.title=Download Resource Pack - %1s + reveal.in_file_manager=Reveal in File Manager + schematics=Schematics schematics.add=Add Schematic Files schematics.add.failed=Failed to add schematic files @@ -1195,6 +1244,7 @@ schematics.info.total_volume=Total Volume schematics.info.version=Schematic Version schematics.manage=Schematics schematics.sub_items=%d sub-item(s) + search=Search search.hint.chinese=Search in English and Chinese search.hint.english=Search in English only @@ -1205,10 +1255,13 @@ search.previous_page=Previous search.next_page=Next search.last_page=Last search.page_n=%1$d / %2$s + selector.choose=Choose selector.choose_file=Choose file selector.custom=Custom + settings=Settings + settings.advanced=Advanced Settings settings.advanced.modify=Edit Advanced Settings settings.advanced.title=Advanced Settings - %s @@ -1281,7 +1334,9 @@ settings.advanced.workaround=Workaround settings.advanced.workaround.warning=Workaround options are intended only for advanced users. Tweaking with these options may crash the game. Unless you know what you are doing, please do not edit these options. settings.advanced.wrapper_launcher=Wrapper Command settings.advanced.wrapper_launcher.prompt=Allows launching using an extra wrapper program like "optirun" on Linux + settings.custom=Custom + settings.game=Settings settings.game.copy_global=Copy from Global Settings settings.game.copy_global.copy_all=Copy All @@ -1304,7 +1359,9 @@ settings.game.working_directory.choose=Choose the working directory settings.game.working_directory.hint=Enable the "Isolated" option in "Working Directory" to allow the current instance to store its settings, saves, and mods in a separate directory.\n\ \n\ It is recommended to enable this option to avoid mod conflicts, but you will need to move your saves manually. + settings.icon=Icon + settings.launcher=Launcher Settings settings.launcher.appearance=Appearance settings.launcher.common_path.tooltip=HMCL will put all game assets and dependencies here. If there are existing libraries in the game directory, then HMCL will prefer to use them first. @@ -1345,6 +1402,7 @@ settings.launcher.title_transparent=Transparent Titlebar settings.launcher.turn_off_animations=Disable Animation (Applies After Restart) settings.launcher.version_list_source=Version List settings.launcher.background.settings.opacity=Opacity + settings.memory=Memory settings.memory.allocate.auto=%1$.1f GiB Minimum / %2$.1f GiB Allocated settings.memory.allocate.auto.exceeded=%1$.1f GiB Minimum / %2$.1f GiB Allocated (%3$.1f GiB Available) @@ -1365,11 +1423,14 @@ settings.type.global.edit=Edit Global Settings settings.type.special.enable=Enable Instance-specific Settings settings.type.special.edit=Edit Current Instance Settings settings.type.special.edit.hint=Current instance "%s" has enabled the "Instance-specific Settings". All options on this page will NOT affect that instance. Click here to edit its own settings. + sponsor=Donors sponsor.bmclapi=Downloads for the Chinese Mainland are provided by BMCLAPI. Click here for more information. sponsor.hmcl=Hello Minecraft! Launcher is a FOSS Minecraft launcher that allows users to manage multiple Minecraft instances easily. Click here for more information. + system.architecture=Architecture system.operating_system=Operating System + terracotta=Multiplayer terracotta.easytier=About EasyTier terracotta.terracotta=Terracotta | Multiplayer @@ -1445,7 +1506,9 @@ terracotta.unsupported=Multiplayer is not yet supported on the current platform. terracotta.unsupported.os.windows.old=Multiplayer requires Windows 10 or later. Please update your system. terracotta.unsupported.arch.32bit=Multiplayer is not supported on 32-bit systems. Please upgrade to a 64-bit system. terracotta.unsupported.arch.loongarch64_ow=Multiplayer is not supported on Linux LoongArch64 Old World distributions. Please update to a New World distribution (such as AOSC OS). + unofficial.hint=You are using an unofficial build of HMCL. We cannot guarantee its security. + update=Update update.accept=Update update.changelog=Changelog @@ -1476,6 +1539,7 @@ update.no_browser=Cannot open in system browser. But we copied the link to your update.tooltip=Update update.preview=Preview HMCL releases early update.preview.tooltip=Enable this option to receive new versions of HMCL early for testing before their official release. + version=Games version.name=Instance Name version.cannot_read=Failed to parse the game instance, installation cannot continue. @@ -1527,14 +1591,17 @@ version.search=Name version.search.prompt=Enter the version name to search version.settings=Settings version.update=Update Modpack + warning.java_interpreted_mode=HMCL is running in an interpreted Java environment, which will greatly affect performance.\n\ \n\ We recommend using a Java with JIT support to open HMCL for the best experience. warning.software_rendering=HMCL is currently using software rendering, which will greatly affect performance. + wiki.tooltip=Minecraft Wiki Page wiki.version.game=https://minecraft.wiki/w/Java_Edition_%s wiki.version.game.snapshot=https://minecraft.wiki/w/Java_Edition_%s + wizard.prev=< Prev wizard.failed=Failed wizard.finish=Finish -wizard.next=Next > +wizard.next=Next > \ No newline at end of file From 48e76e433efc21e3f81a88b620bd0f17a5a61e98 Mon Sep 17 00:00:00 2001 From: mine_ Date: Sun, 23 Nov 2025 10:00:31 +0800 Subject: [PATCH 28/66] =?UTF-8?q?feat:=20i18n,=E5=B0=86copy=E6=94=B9?= =?UTF-8?q?=E4=B8=BAduplicate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/WorldListItemSkin.java | 2 +- .../hmcl/ui/versions/WorldManagePage.java | 2 +- .../hmcl/ui/versions/WorldManageUIUtils.java | 12 ++++++------ .../src/main/resources/assets/lang/I18N.properties | 14 +++++++------- .../main/resources/assets/lang/I18N_es.properties | 6 +++--- .../main/resources/assets/lang/I18N_ru.properties | 6 +++--- .../main/resources/assets/lang/I18N_uk.properties | 6 +++--- .../main/resources/assets/lang/I18N_zh.properties | 14 +++++++------- .../resources/assets/lang/I18N_zh_CN.properties | 14 +++++++------- 9 files changed, 38 insertions(+), 38 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java index 6dcd394595..420d9ba9e7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java @@ -137,7 +137,7 @@ public void showPopupMenu(JFXPopup.PopupHPosition hPosition, double initOffsetX, new MenuSeparator(), new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), item::export, popup), new IconedMenuItem(SVG.DELETE, i18n("world.delete"), item::delete, popup), - new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.copy"), item::copy, popup), + new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), item::copy, popup), new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.world"), item::reveal, popup)); JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java index e498b14e9f..fd95d8f060 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java @@ -125,7 +125,7 @@ public WorldManagePage(World world, Path backupsDir) { managePopupMenu.getContent().addAll( new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> WorldManageUIUtils.export(world, sessionLockChannel), managePopup), new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> WorldManageUIUtils.delete(world, () -> fireEvent(new PageCloseEvent()), sessionLockChannel), managePopup), - new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.copy"), () -> WorldManageUIUtils.copyWorld(world, null, sessionLockChannel), managePopup) + new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), () -> WorldManageUIUtils.copyWorld(world, null, sessionLockChannel), managePopup) ); toolbar.addNavigationDrawerItem(i18n("settings.game.management"), SVG.MENU, null, managePopupMenuItem -> diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java index efb197937f..7702d24596 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java @@ -94,22 +94,22 @@ public static void copyWorld(World world, Runnable runnable) { public static void copyWorld(World world, Runnable runnable, FileChannel sessionLockChannel) { Path worldPath = world.getFile(); Controllers.dialog(new InputDialogPane( - i18n("world.copy.prompt"), + i18n("world.duplicate.prompt"), "", (result, resolve, reject) -> { if (StringUtils.isBlank(result)) { - reject.accept(i18n("world.copy.failed.empty_name")); + reject.accept(i18n("world.duplicate.failed.empty_name")); return; } if (result.contains("/") || result.contains("\\") || !FileUtils.isNameValid(result)) { - reject.accept(i18n("world.copy.failed.invalid_name")); + reject.accept(i18n("world.duplicate.failed.invalid_name")); return; } Path targetDir = worldPath.resolveSibling(result); if (Files.exists(targetDir)) { - reject.accept(i18n("world.copy.failed.already_exists")); + reject.accept(i18n("world.duplicate.failed.already_exists")); return; } @@ -121,7 +121,7 @@ public static void copyWorld(World world, Runnable runnable, FileChannel session } Task.runAsync(Schedulers.io(), () -> world.copy(result)) - .thenAcceptAsync(Schedulers.javafx(), (Void) -> Controllers.showToast(i18n("world.copy.success.toast"))) + .thenAcceptAsync(Schedulers.javafx(), (Void) -> Controllers.showToast(i18n("world.duplicate.success.toast"))) .thenAcceptAsync(Schedulers.javafx(), (Void) -> { if (runnable != null) { runnable.run(); @@ -131,7 +131,7 @@ public static void copyWorld(World world, Runnable runnable, FileChannel session if (exception == null) { resolve.run(); } else { - reject.accept(i18n("world.copy.failed")); + reject.accept(i18n("world.duplicate.failed")); } }) .start(); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index c0aef93943..5a61842014 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1127,13 +1127,13 @@ world.chunkbase.end_city=End City world.chunkbase.seed_map=Seed Map world.chunkbase.stronghold=Stronghold world.chunkbase.nether_fortress=Nether Fortress -world.copy=Copy the World -world.copy.prompt=Please enter the name of the copied world -world.copy.failed.already_exists=Directory already exists -world.copy.failed.empty_name=Name cannot be empty -world.copy.failed.invalid_name=Name contains invalid characters -world.copy.failed=Copy world failed -world.copy.success.toast=Copy world success +world.duplicate=Duplicate the World +world.duplicate.prompt=Please enter the name of the duplicated world +world.duplicate.failed.already_exists=Directory already exists +world.duplicate.failed.empty_name=Name cannot be empty +world.duplicate.failed.invalid_name=Name contains invalid characters +world.duplicate.failed=Duplicate world failed +world.duplicate.success.toast=Duplicate world success world.datapack=Datapacks world.datetime=Last played on %s world.delete=Delete the World diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index 47fcbd9373..028977a7d3 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -1128,9 +1128,9 @@ world.chunkbase.end_city=Ciudad del End world.chunkbase.seed_map=Vista previa de la generación mundial world.chunkbase.stronghold=Fortaleza world.chunkbase.nether_fortress=Fortaleza del Nether -world.copy.failed.already_exists=El directorio ya existe -world.copy.failed.empty_name=El nombre no puede estar vacío -world.copy.failed.invalid_name=El nombre contiene caracteres no válidos +world.duplicate.failed.already_exists=El directorio ya existe +world.duplicate.failed.empty_name=El nombre no puede estar vacío +world.duplicate.failed.invalid_name=El nombre contiene caracteres no válidos world.datapack=Paquetes de datos world.datetime=Jugado por última vez en %s world.delete=Eliminar este mundo diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index b5ba3da75c..a1cf53f435 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -1123,9 +1123,9 @@ world.chunkbase.end_city=Город Края world.chunkbase.seed_map=Предпросмотр генерации мира world.chunkbase.stronghold=Крепость world.chunkbase.nether_fortress=Крепость Нижнего мира -world.copy.failed.already_exists=Папка уже существует -world.copy.failed.empty_name=Название не может быть пустым -world.copy.failed.invalid_name=Название содержит недопустимые символы +world.duplicate.failed.already_exists=Папка уже существует +world.duplicate.failed.empty_name=Название не может быть пустым +world.duplicate.failed.invalid_name=Название содержит недопустимые символы world.delete=Удалить этот мир world.delete.failed=Не удалось удалить мир.\n%s world.datapack=Наборы данных diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index cf0887bbac..aeca56bfd8 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -1065,9 +1065,9 @@ world.chunkbase.end_city=Кінцеве місто world.chunkbase.seed_map=Карта насіння world.chunkbase.stronghold=Фортеця world.chunkbase.nether_fortress=Форт Незеру -world.copy.failed.already_exists=Каталог вже існує -world.copy.failed.empty_name=Назва не може бути порожньою -world.copy.failed.invalid_name=Назва містить недійсні символи +world.duplicate.failed.already_exists=Каталог вже існує +world.duplicate.failed.empty_name=Назва не може бути порожньою +world.duplicate.failed.invalid_name=Назва містить недійсні символи world.datapack=Datapacks world.datetime=Останній раз грали %s world.delete=Видалити цей світ diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 7645a05b1e..052716e36c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -924,13 +924,13 @@ world.chunkbase.end_city=終界城地圖 world.chunkbase.seed_map=種子地圖 world.chunkbase.stronghold=要塞地圖 world.chunkbase.nether_fortress=地獄要塞地圖 -world.copy=複製此世界 -world.copy.prompt=輸入複製後的世界名稱 -world.copy.failed.already_exists=目錄已存在 -world.copy.failed.empty_name=名稱不能為空 -world.copy.failed.invalid_name=名稱中包含無效字元 -world.copy.failed=複製世界失敗 -world.copy.success.toast=複製世界成功 +world.duplicate=複製此世界 +world.duplicate.prompt=輸入複製後的世界名稱 +world.duplicate.failed.already_exists=目錄已存在 +world.duplicate.failed.empty_name=名稱不能為空 +world.duplicate.failed.invalid_name=名稱中包含無效字元 +world.duplicate.failed=複製世界失敗 +world.duplicate.success.toast=複製世界成功 world.datapack=資料包管理 world.datetime=上一次遊戲時間: %s world.delete=刪除此世界 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index f6c8b07335..c6dac9f933 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -934,13 +934,13 @@ world.chunkbase.end_city=末地城地图 world.chunkbase.seed_map=种子地图 world.chunkbase.stronghold=要塞地图 world.chunkbase.nether_fortress=下界要塞地图 -world.copy=复制此世界 -world.copy.prompt=输入复制后的世界名称 -world.copy.failed.already_exists=文件夹已存在 -world.copy.failed.empty_name=名称不能为空 -world.copy.failed.invalid_name=名称中包含非法字符 -world.copy.failed=复制世界失败 -world.copy.success.toast=复制世界成功 +world.duplicate=复制此世界 +world.duplicate.prompt=输入复制后的世界名称 +world.duplicate.failed.already_exists=文件夹已存在 +world.duplicate.failed.empty_name=名称不能为空 +world.duplicate.failed.invalid_name=名称中包含非法字符 +world.duplicate.failed=复制世界失败 +world.duplicate.success.toast=复制世界成功 world.datapack=数据包管理 world.datetime=上一次游戏时间: %s world.delete=删除此世界 From 765e537b4ae5de4990c315474fd15d7b764ff1c7 Mon Sep 17 00:00:00 2001 From: mine_ Date: Fri, 28 Nov 2025 17:53:11 +0800 Subject: [PATCH 29/66] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java | 1 + 1 file changed, 1 insertion(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java index 420d9ba9e7..8f0da2c123 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java @@ -138,6 +138,7 @@ public void showPopupMenu(JFXPopup.PopupHPosition hPosition, double initOffsetX, new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), item::export, popup), new IconedMenuItem(SVG.DELETE, i18n("world.delete"), item::delete, popup), new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), item::copy, popup), + new MenuSeparator(), new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.world"), item::reveal, popup)); JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup); From fc3727adc96a3739a915ad3baafe487fb173f7c0 Mon Sep 17 00:00:00 2001 From: mine_ Date: Sun, 7 Dec 2025 09:45:02 +0800 Subject: [PATCH 30/66] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java | 5 ++--- .../java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index 36d23eec66..4b2fb6ff65 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -46,7 +46,6 @@ import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.*; -import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.jetbrains.annotations.PropertyKey; import java.io.File; @@ -156,7 +155,7 @@ private void updateControls() { iconImageView.setImage(world.getIcon() == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : world.getIcon()); iconImageView.setCursor(Cursor.HAND); - Node editIcon = SVG.EDIT.createIcon(Theme.blackFill(), 12); + Node editIcon = SVG.EDIT.createIcon(12); editIcon.setDisable(worldManagePage.isReadOnly()); editIcon.setCursor(Cursor.HAND); Runnable onClickAction = () -> Controllers.confirm( @@ -383,7 +382,7 @@ private void updateControls() { setRightTextLabel(spawnPane, spawnLabel, () -> { Dimension dimension; - if (GameVersionNumber.asGameVersion(world.getGameVersion()).compareTo("25w07a") >= 0) { + if (world.getGameVersion().compareTo("25w07a") >= 0) { CompoundTag respawnTag = player.get("respawn"); if (respawnTag == null) { return ""; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java index 273be5bea3..1bf6b470b2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java @@ -34,7 +34,6 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.util.ChunkBaseApp; import org.jackhuang.hmcl.util.StringUtils; -import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import java.io.IOException; import java.nio.channels.FileChannel; From 2fc124a8541ac55dfffdc79da94f42daad1c28e2 Mon Sep 17 00:00:00 2001 From: mine_ Date: Thu, 11 Dec 2025 23:20:54 +0800 Subject: [PATCH 31/66] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=9B=BE?= =?UTF-8?q?=E6=A0=87=E5=8A=A0=E8=BD=BD=E5=A4=B1=E8=B4=A5=E6=97=B6=E7=9A=84?= =?UTF-8?q?=E5=BC=B9=E7=AA=97=E5=92=8C=E6=88=90=E5=8A=9F=E6=97=B6=E7=9A=84?= =?UTF-8?q?=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/versions/WorldInfoPage.java | 14 ++++++++++---- .../src/main/resources/assets/lang/I18N.properties | 3 +++ .../main/resources/assets/lang/I18N_zh.properties | 3 +++ .../resources/assets/lang/I18N_zh_CN.properties | 3 +++ 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index 4b2fb6ff65..e445f2252e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -597,9 +597,7 @@ static Dimension of(Tag tag) { } String formatPosition(Tag tag) { - if (tag instanceof ListTag listTag) { - if (listTag.size() != 3) - return null; + if (tag instanceof ListTag listTag && listTag.size() == 3) { Tag x = listTag.get(0); Tag y = listTag.get(1); @@ -681,7 +679,14 @@ private void changeWorldIcon() { File file = fileChooser.showOpenDialog(Controllers.getStage()); if (file == null) return; - Image original = new Image(file.toURI().toString()); + Image original; + try { + original = FXUtils.loadImage(file.toPath()); + } catch (Exception e) { + LOG.warning("Failed to load image", e); + Controllers.dialog(i18n("world.icon.change.fail.text"), i18n("world.icon.change.fail.title"), MessageDialogPane.MessageType.ERROR); + return; + } Image squareImage = cropCenterSquare(original); Image finalImage; @@ -719,6 +724,7 @@ private void saveImage(Image image, Path path) { try { PNGJavaFXUtils.writeImage(image, path); iconImageView.setImage(image); + Controllers.showToast(i18n("world.icon.change.succeed.toast")); } catch (IOException e) { LOG.warning(e.getMessage()); } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 978796186f..ebeb8d6bc1 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1149,6 +1149,9 @@ world.extension=World Archive world.game_version=Game Version world.icon=World Icon world.icon.change=Change world icon +world.icon.change.fail.title=Failed to parse image +world.icon.change.fail.text=This image appears to be corrupted, and HMCL cannot parse it. +world.icon.change.succeed.toast=World icon updated successfully. world.icon.change.tip=Please provide a 64x64 PNG image. If the provided image does not meet the requirements, HMCL will automatically crop and resize it to 64x64. world.icon.choose.title=Select world icon world.import.already_exists=This world already exists. diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 57308e1674..0dec0aae24 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -945,6 +945,9 @@ world.export.wizard=匯出世界「%s」 world.extension=存檔壓縮檔 world.icon=世界圖示 world.icon.change=修改世界圖示 +world.icon.change.fail.title=圖片解析失敗 +world.icon.change.fail.text=該圖片似乎已損毀,HMCL 無法解析它 +world.icon.change.succeed.toast=世界圖標修改成功 world.icon.change.tip=請提供一張解析度為 64×64、格式為 PNG 的圖片。若提供的圖片不符規格,HMCL 將會自動裁切並調整為 64×64 的解析度。 world.icon.choose.title=選擇世界圖示 world.import.already_exists=此世界已經存在 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 59ef12525e..f0ed4bdfdd 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -956,6 +956,9 @@ world.extension=世界压缩包 world.game_version=游戏版本 world.icon=世界图标 world.icon.change=修改世界图标 +world.icon.change.fail.title=图片解析出错 +world.icon.change.fail.text=该图片似乎已损坏,HMCL 无法解析它 +world.icon.change.succeed.toast=世界图标修改成功 world.icon.change.tip=请提供一张分辨率为64×64、格式为PNG的图片。如果提供的图片不符合要求,HMCL将会自动裁剪并调整为64×64的分辨率。 world.icon.choose.title=选择世界图标 world.import.already_exists=此世界已经存在 From 32c231b730f52ab8f3427eca288e3b743231ea91 Mon Sep 17 00:00:00 2001 From: mineDiamond Date: Fri, 12 Dec 2025 10:15:04 +0800 Subject: [PATCH 32/66] Apply i18n suggestions from code review Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com> --- HMCL/src/main/resources/assets/lang/I18N.properties | 8 ++++---- HMCL/src/main/resources/assets/lang/I18N_zh.properties | 4 ++-- HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index ebeb8d6bc1..0d76c1c4b8 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1133,8 +1133,8 @@ world.duplicate.prompt=Please enter the name of the duplicated world world.duplicate.failed.already_exists=Directory already exists world.duplicate.failed.empty_name=Name cannot be empty world.duplicate.failed.invalid_name=Name contains invalid characters -world.duplicate.failed=Duplicate world failed -world.duplicate.success.toast=Duplicate world success +world.duplicate.failed=Failed to duplicate the world +world.duplicate.success.toast=Successfully duplicated the world world.datapack=Datapacks world.datetime=Last played on %s world.delete=Delete the World @@ -1151,8 +1151,8 @@ world.icon=World Icon world.icon.change=Change world icon world.icon.change.fail.title=Failed to parse image world.icon.change.fail.text=This image appears to be corrupted, and HMCL cannot parse it. -world.icon.change.succeed.toast=World icon updated successfully. -world.icon.change.tip=Please provide a 64x64 PNG image. If the provided image does not meet the requirements, HMCL will automatically crop and resize it to 64x64. +world.icon.change.succeed.toast=Successfully updated the world icon. +world.icon.change.tip=A 64x64 PNG image is required. HMCL will automatically crop and resize any non-conforming images to 64x64. world.icon.choose.title=Select world icon world.import.already_exists=This world already exists. world.import.choose=Choose world archive you want to import diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 0dec0aae24..4f93f2e949 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -947,8 +947,8 @@ world.icon=世界圖示 world.icon.change=修改世界圖示 world.icon.change.fail.title=圖片解析失敗 world.icon.change.fail.text=該圖片似乎已損毀,HMCL 無法解析它 -world.icon.change.succeed.toast=世界圖標修改成功 -world.icon.change.tip=請提供一張解析度為 64×64、格式為 PNG 的圖片。若提供的圖片不符規格,HMCL 將會自動裁切並調整為 64×64 的解析度。 +world.icon.change.succeed.toast=世界圖示修改成功 +world.icon.change.tip=請提供一張 64x64 PNG 格式的圖片。對於不符合要求的圖片,HMCL 將自動裁剪並縮放至 64x64。 world.icon.choose.title=選擇世界圖示 world.import.already_exists=此世界已經存在 world.import.choose=選取要匯入的存檔壓縮檔 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index f0ed4bdfdd..cfd08da37a 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -959,7 +959,7 @@ world.icon.change=修改世界图标 world.icon.change.fail.title=图片解析出错 world.icon.change.fail.text=该图片似乎已损坏,HMCL 无法解析它 world.icon.change.succeed.toast=世界图标修改成功 -world.icon.change.tip=请提供一张分辨率为64×64、格式为PNG的图片。如果提供的图片不符合要求,HMCL将会自动裁剪并调整为64×64的分辨率。 +world.icon.change.tip=请提供一张 64x64 PNG 格式的图片。对于不符合要求的图片,HMCL 将自动裁剪并缩放至 64x64。 world.icon.choose.title=选择世界图标 world.import.already_exists=此世界已经存在 world.import.choose=选择要导入的存档压缩包 From 98d77b772fccefb62f70e8dca2329c4dfe98964a Mon Sep 17 00:00:00 2001 From: mine_ Date: Fri, 12 Dec 2025 13:10:34 +0800 Subject: [PATCH 33/66] =?UTF-8?q?feat:=20=E5=86=8D=E6=AC=A1=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E9=87=8D=E7=94=9F=E7=82=B9=E5=8F=AF=E8=83=BD=E6=97=A0?= =?UTF-8?q?=E6=B3=95=E8=A2=AB=E8=AF=BB=E5=8F=96=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/WorldInfoPage.java | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index e445f2252e..130a16f984 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -361,7 +361,7 @@ private void updateControls() { setLeftLabel(lastDeathLocationPane, "world.info.player.last_death_location"); Label lastDeathLocationLabel = new Label(); setRightTextLabel(lastDeathLocationPane, lastDeathLocationLabel, () -> { - Tag tag = player.get("LastDeathLocation"); + Tag tag = player.get("LastDeathLocation");//Valid after 22w14a; prior to this version, the game did not record the last death location data. if (tag instanceof CompoundTag compoundTag) { Dimension dim = Dimension.of(compoundTag.get("dimension")); if (dim != null) { @@ -382,29 +382,25 @@ private void updateControls() { setRightTextLabel(spawnPane, spawnLabel, () -> { Dimension dimension; - if (world.getGameVersion().compareTo("25w07a") >= 0) { - CompoundTag respawnTag = player.get("respawn"); - if (respawnTag == null) { - return ""; - } + if (player.get("respawn") instanceof CompoundTag respawnTag) {//Valid after 25w07a dimension = Dimension.of(respawnTag.get("dimension")); Tag posTag = respawnTag.get("pos"); if (posTag instanceof IntArrayTag intArrayTag) { return dimension.formatPosition(intArrayTag.getValue(0), intArrayTag.getValue(1), intArrayTag.getValue(2)); } - } else { - dimension = Dimension.of(player.get("SpawnDimension")); + } else if (player.get("SpawnX") instanceof IntTag intX + && player.get("SpawnY") instanceof IntTag intY + && player.get("SpawnZ") instanceof IntTag intZ) {//Valid before 25w07a + //SpawnDimension tag is valid after 20w12a. Prior to this version, the game did not record the respawn point dimension and respawned in the Overworld. + dimension = Dimension.of(player.get("SpawnDimension") == null ? new IntTag("SpawnDimension", 0) : player.get("SpawnDimension")); if (dimension == null) { return ""; } - Tag x = player.get("SpawnX"); - Tag y = player.get("SpawnY"); - Tag z = player.get("SpawnZ"); - if (x instanceof IntTag intX && y instanceof IntTag intY && z instanceof IntTag intZ) - return dimension.formatPosition(intX.getValue(), intY.getValue(), intZ.getValue()); + return dimension.formatPosition(intX.getValue(), intY.getValue(), intZ.getValue()); } + return ""; }); } @@ -546,7 +542,7 @@ private void setLeftLabel(BorderPane borderPane, @PropertyKey(resourceBundle = " private void setRightTextField(BorderPane borderPane, JFXTextField textField, int perfWidth) { textField.setDisable(worldManagePage.isDisable()); textField.setPrefWidth(perfWidth); - BorderPane.setAlignment(textField, Pos.CENTER_RIGHT); + textField.setAlignment(Pos.CENTER_RIGHT); borderPane.setRight(textField); } From 933f8f3e0aca87885c7eea34b4349212e246e147 Mon Sep 17 00:00:00 2001 From: mine_ Date: Fri, 12 Dec 2025 13:20:28 +0800 Subject: [PATCH 34/66] fix style --- .../java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index 130a16f984..8a2e3b2c2b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -382,7 +382,7 @@ private void updateControls() { setRightTextLabel(spawnPane, spawnLabel, () -> { Dimension dimension; - if (player.get("respawn") instanceof CompoundTag respawnTag) {//Valid after 25w07a + if (player.get("respawn") instanceof CompoundTag respawnTag) { //Valid after 25w07a dimension = Dimension.of(respawnTag.get("dimension")); Tag posTag = respawnTag.get("pos"); @@ -391,7 +391,7 @@ private void updateControls() { } } else if (player.get("SpawnX") instanceof IntTag intX && player.get("SpawnY") instanceof IntTag intY - && player.get("SpawnZ") instanceof IntTag intZ) {//Valid before 25w07a + && player.get("SpawnZ") instanceof IntTag intZ) { //Valid before 25w07a //SpawnDimension tag is valid after 20w12a. Prior to this version, the game did not record the respawn point dimension and respawned in the Overworld. dimension = Dimension.of(player.get("SpawnDimension") == null ? new IntTag("SpawnDimension", 0) : player.get("SpawnDimension")); if (dimension == null) { From b69e4c7b13a0859e21241a51bc09f1a4c4ad54e8 Mon Sep 17 00:00:00 2001 From: mine_ Date: Fri, 12 Dec 2025 21:29:05 +0800 Subject: [PATCH 35/66] =?UTF-8?q?fix:=20=E5=B0=8F=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java | 1 + 1 file changed, 1 insertion(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index 8a2e3b2c2b..f3f7cac5bf 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -154,6 +154,7 @@ private void updateControls() { FXUtils.limitSize(iconImageView, 32, 32); iconImageView.setImage(world.getIcon() == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : world.getIcon()); iconImageView.setCursor(Cursor.HAND); + iconImageView.setDisable(worldManagePage.isReadOnly()); Node editIcon = SVG.EDIT.createIcon(12); editIcon.setDisable(worldManagePage.isReadOnly()); From 85008ab479101e1c6bcf3c5b7e71dd165924ee18 Mon Sep 17 00:00:00 2001 From: mine_ Date: Sat, 13 Dec 2025 12:49:01 +0800 Subject: [PATCH 36/66] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/WorldInfoPage.java | 197 ++++++++---------- 1 file changed, 87 insertions(+), 110 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index f3f7cac5bf..fad2b152d0 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -175,42 +175,44 @@ private void updateControls() { iconPane.setRight(hBox); } - BorderPane randomSeedPane = new BorderPane(); + BorderPane seedPane = new BorderPane(); { - - HBox left = new HBox(8); - BorderPane.setAlignment(left, Pos.CENTER_LEFT); - left.setAlignment(Pos.CENTER_LEFT); - randomSeedPane.setLeft(left); - - Label label = new Label(i18n("world.info.random_seed")); + setLeftLabel(seedPane, "world.info.random_seed"); SimpleBooleanProperty visibility = new SimpleBooleanProperty(); StackPane visibilityButton = new StackPane(); - visibilityButton.setCursor(Cursor.HAND); - FXUtils.setLimitWidth(visibilityButton, 12); - FXUtils.setLimitHeight(visibilityButton, 12); - FXUtils.onClicked(visibilityButton, () -> visibility.set(!visibility.get())); + { + visibilityButton.setCursor(Cursor.HAND); + FXUtils.setLimitWidth(visibilityButton, 12); + FXUtils.setLimitHeight(visibilityButton, 12); + FXUtils.onClicked(visibilityButton, () -> visibility.set(!visibility.get())); + } - left.getChildren().setAll(label, visibilityButton); + Label seedLabel = new Label(); + { + FXUtils.copyOnDoubleClick(seedLabel); + BorderPane.setAlignment(seedLabel, Pos.CENTER_RIGHT); - Label randomSeedLabel = new Label(); - FXUtils.copyOnDoubleClick(randomSeedLabel); - BorderPane.setAlignment(randomSeedLabel, Pos.CENTER_RIGHT); - randomSeedPane.setRight(randomSeedLabel); + Tag tag = worldGenSettings != null ? worldGenSettings.get("seed") : dataTag.get("RandomSeed"); + if (tag instanceof LongTag) { + seedLabel.setText(tag.getValue().toString()); + } - Tag tag = worldGenSettings != null ? worldGenSettings.get("seed") : dataTag.get("RandomSeed"); - if (tag instanceof LongTag) { - randomSeedLabel.setText(tag.getValue().toString()); + BoxBlur blur = new BoxBlur(); + blur.setIterations(3); + FXUtils.onChangeAndOperate(visibility, isVisibility -> { + SVG icon = isVisibility ? SVG.VISIBILITY : SVG.VISIBILITY_OFF; + visibilityButton.getChildren().setAll(icon.createIcon(12)); + seedLabel.setEffect(isVisibility ? null : blur); + }); } - BoxBlur blur = new BoxBlur(); - blur.setIterations(3); - FXUtils.onChangeAndOperate(visibility, isVisibility -> { - SVG icon = isVisibility ? SVG.VISIBILITY : SVG.VISIBILITY_OFF; - visibilityButton.getChildren().setAll(icon.createIcon(12)); - randomSeedLabel.setEffect(isVisibility ? null : blur); - }); + HBox right = new HBox(8); + { + BorderPane.setAlignment(right, Pos.CENTER_RIGHT); + right.getChildren().setAll(visibilityButton, seedLabel); + seedPane.setRight(right); + } } BorderPane lastPlayedPane = new BorderPane(); @@ -242,20 +244,7 @@ private void updateControls() { allowCheatsButton.setDisable(worldManagePage.isReadOnly()); Tag tag = dataTag.get("allowCommands"); - if (tag instanceof ByteTag byteTag) { - byte value = byteTag.getValue(); - if (value == 0 || value == 1) { - allowCheatsButton.setSelected(value == 1); - allowCheatsButton.selectedProperty().addListener((o, oldValue, newValue) -> { - byteTag.setValue(newValue ? (byte) 1 : (byte) 0); - saveLevelDat(); - }); - } else { - allowCheatsButton.setDisable(true); - } - } else { - allowCheatsButton.setDisable(true); - } + checkTagAndSetListener(tag, allowCheatsButton); } OptionToggleButton generateFeaturesButton = new OptionToggleButton(); @@ -264,20 +253,7 @@ private void updateControls() { generateFeaturesButton.setDisable(worldManagePage.isReadOnly()); Tag tag = worldGenSettings != null ? worldGenSettings.get("generate_features") : dataTag.get("MapFeatures"); - if (tag instanceof ByteTag byteTag) { - byte value = byteTag.getValue(); - if (value == 0 || value == 1) { - generateFeaturesButton.setSelected(value == 1); - generateFeaturesButton.selectedProperty().addListener((o, oldValue, newValue) -> { - byteTag.setValue(newValue ? (byte) 1 : (byte) 0); - saveLevelDat(); - }); - } else { - generateFeaturesButton.setDisable(true); - } - } else { - generateFeaturesButton.setDisable(true); - } + checkTagAndSetListener(tag, generateFeaturesButton); } BorderPane difficultyPane = new BorderPane(); @@ -314,25 +290,11 @@ private void updateControls() { difficultyLockPane.setDisable(worldManagePage.isReadOnly()); Tag tag = dataTag.get("DifficultyLocked"); - - if (tag instanceof ByteTag byteTag) { - byte value = byteTag.getValue(); - if (value == 0 || value == 1) { - difficultyLockPane.setSelected(value == 1); - difficultyLockPane.selectedProperty().addListener((o, oldValue, newValue) -> { - byteTag.setValue(newValue ? (byte) 1 : (byte) 0); - saveLevelDat(); - }); - } else { - difficultyLockPane.setDisable(true); - } - } else { - difficultyLockPane.setDisable(true); - } + checkTagAndSetListener(tag, difficultyLockPane); } basicInfo.getContent().setAll( - worldNamePane, gameVersionPane, iconPane, randomSeedPane, lastPlayedPane, timePane, + worldNamePane, gameVersionPane, iconPane, seedPane, lastPlayedPane, timePane, allowCheatsButton, generateFeaturesButton, difficultyPane, difficultyLockPane); rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info.basic")), basicInfo); @@ -455,19 +417,7 @@ private void updateControls() { Tag tag = player.get("Health"); if (tag instanceof FloatTag floatTag) { - healthField.setText(new DecimalFormat("#").format(floatTag.getValue().floatValue())); - - healthField.textProperty().addListener((o, oldValue, newValue) -> { - if (newValue != null) { - try { - floatTag.setValue(Float.parseFloat(newValue)); - saveLevelDat(); - } catch (Throwable ignored) { - } - } - }); - FXUtils.setValidateWhileTextChanged(healthField, true); - healthField.setValidators(new DoubleValidator(i18n("input.number"), true)); + setTagAndTextFiled(floatTag, healthField); } else { healthField.setDisable(true); } @@ -481,19 +431,7 @@ private void updateControls() { Tag tag = player.get("foodLevel"); if (tag instanceof IntTag intTag) { - foodLevelField.setText(String.valueOf(intTag.getValue())); - - foodLevelField.textProperty().addListener((o, oldValue, newValue) -> { - if (newValue != null) { - try { - intTag.setValue(Integer.parseInt(newValue)); - saveLevelDat(); - } catch (Throwable ignored) { - } - } - }); - FXUtils.setValidateWhileTextChanged(foodLevelField, true); - foodLevelField.setValidators(new NumberValidator(i18n("input.number"), true)); + setTagAndTextFiled(intTag, foodLevelField); } else { foodLevelField.setDisable(true); } @@ -507,19 +445,7 @@ private void updateControls() { Tag tag = player.get("XpLevel"); if (tag instanceof IntTag intTag) { - xpLevelField.setText(String.valueOf(intTag.getValue())); - - xpLevelField.textProperty().addListener((o, oldValue, newValue) -> { - if (newValue != null) { - try { - intTag.setValue(Integer.parseInt(newValue)); - saveLevelDat(); - } catch (Throwable ignored) { - } - } - }); - FXUtils.setValidateWhileTextChanged(xpLevelField, true); - xpLevelField.setValidators(new NumberValidator(i18n("input.number"), true)); + setTagAndTextFiled(intTag, xpLevelField); } else { xpLevelField.setDisable(true); } @@ -558,6 +484,57 @@ private void setRightTextLabel(BorderPane borderPane, Label label, Callable { + byteTag.setValue((byte) (newValue ? 1 : 0)); + saveLevelDat(); + }); + } else { + toggleButton.setDisable(true); + } + } else { + toggleButton.setDisable(true); + } + } + + private void setTagAndTextFiled(IntTag intTag, JFXTextField jfxTextField) { + jfxTextField.setText(String.valueOf(intTag.getValue())); + + jfxTextField.textProperty().addListener((o, oldValue, newValue) -> { + if (newValue != null) { + try { + intTag.setValue(Integer.parseInt(newValue)); + saveLevelDat(); + } catch (Exception e) { + jfxTextField.setText(oldValue); + LOG.warning("Exception happened when saving level.dat", e); + } + } + }); + FXUtils.setValidateWhileTextChanged(jfxTextField, true); + jfxTextField.setValidators(new NumberValidator(i18n("input.number"), true)); + } + + private void setTagAndTextFiled(FloatTag floatTag, JFXTextField jfxTextField) { + jfxTextField.setText(new DecimalFormat("#").format(floatTag.getValue().floatValue())); + + jfxTextField.textProperty().addListener((o, oldValue, newValue) -> { + if (newValue != null) { + try { + floatTag.setValue(Float.parseFloat(newValue)); + saveLevelDat(); + } catch (Throwable ignored) { + } + } + }); + FXUtils.setValidateWhileTextChanged(jfxTextField, true); + jfxTextField.setValidators(new DoubleValidator(i18n("input.number"), true)); + } + private void saveLevelDat() { LOG.info("Saving level.dat of world " + world.getWorldName()); try { From 987e0327d6b989eb0c3321c00c46eb6296690fb4 Mon Sep 17 00:00:00 2001 From: mine_ Date: Sat, 13 Dec 2025 13:01:44 +0800 Subject: [PATCH 37/66] =?UTF-8?q?feat:=20=E5=B0=8F=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/versions/WorldInfoPage.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index fad2b152d0..435a063fcb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -490,8 +490,13 @@ private void checkTagAndSetListener(Tag tag, OptionToggleButton toggleButton) { if (value == 0 || value == 1) { toggleButton.setSelected(value == 1); toggleButton.selectedProperty().addListener((o, oldValue, newValue) -> { - byteTag.setValue((byte) (newValue ? 1 : 0)); - saveLevelDat(); + try { + byteTag.setValue((byte) (newValue ? 1 : 0)); + saveLevelDat(); + } catch (Exception e) { + toggleButton.setSelected(oldValue); + LOG.warning("Exception happened when saving level.dat", e); + } }); } else { toggleButton.setDisable(true); @@ -527,7 +532,9 @@ private void setTagAndTextFiled(FloatTag floatTag, JFXTextField jfxTextField) { try { floatTag.setValue(Float.parseFloat(newValue)); saveLevelDat(); - } catch (Throwable ignored) { + } catch (Exception e) { + jfxTextField.setText(oldValue); + LOG.warning("Exception happened when saving level.dat", e); } } }); From d34ca4917d47052371a11813ddff153200bd4395 Mon Sep 17 00:00:00 2001 From: mine_ Date: Thu, 18 Dec 2025 09:36:27 +0800 Subject: [PATCH 38/66] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/versions/WorldManagePage.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java index 1bf6b470b2..af3857b706 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java @@ -138,17 +138,15 @@ public WorldManagePage(World world, Path backupsDir) { BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0)); left.setBottom(toolbar); - // Does it need to be done in the background? - try { - sessionLockChannel = world.lock(); - LOG.info("Acquired lock on world " + world.getFileName()); - } catch (IOException ignored) { - } - + getSessionLock(); this.addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated); } private void onNavigated(Navigator.NavigationEvent event) { + getSessionLock(); + } + + private void getSessionLock() { try { sessionLockChannel = world.lock(); LOG.info("Acquired lock on world " + world.getFileName()); From 98ee1739ff8b42b5e7320a0f59ecf2fa27dc12ec Mon Sep 17 00:00:00 2001 From: mine_ Date: Sat, 20 Dec 2025 21:38:34 +0800 Subject: [PATCH 39/66] =?UTF-8?q?feat:=20=E5=B0=86=E8=8E=B7=E5=8F=96/?= =?UTF-8?q?=E5=85=B3=E9=97=ADSessionLockChannel=E7=9A=84=E6=96=B9=E6=B3=95?= =?UTF-8?q?=E4=B9=9F=E7=BB=9F=E4=B8=80=E5=9C=A8WorldManageUIUtils=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/WorldManagePage.java | 29 +++++++------------ .../hmcl/ui/versions/WorldManageUIUtils.java | 12 +++++++- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java index 57b5120683..cc0bd6a722 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java @@ -66,7 +66,6 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco public WorldManagePage(World world, Path backupsDir, Profile profile, String id) { this.world = world; this.backupsDir = backupsDir; - this.profile = profile; this.id = id; @@ -159,18 +158,23 @@ public WorldManagePage(World world, Path backupsDir, Profile profile, String id) this.addEventHandler(Navigator.NavigationEvent.EXITED, this::onExited); - getSessionLock(); + try { + WorldManageUIUtils.getSessionLockChannel(world, sessionLockChannel); + } catch (IOException ignored) { + } this.addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated); } private void onNavigated(Navigator.NavigationEvent event) { - getSessionLock(); + try { + WorldManageUIUtils.getSessionLockChannel(world, sessionLockChannel); + } catch (IOException ignored) { + } } - private void getSessionLock() { + public void onExited(Navigator.NavigationEvent event) { try { - sessionLockChannel = world.lock(); - LOG.info("Acquired lock on world " + world.getFileName()); + WorldManageUIUtils.closeSessionLockChannel(world, sessionLockChannel); } catch (IOException ignored) { } } @@ -192,19 +196,6 @@ public boolean isReadOnly() { return sessionLockChannel == null; } - public void onExited(Navigator.NavigationEvent event) { - if (sessionLockChannel != null) { - try { - sessionLockChannel.close(); - LOG.info("Releases the lock on world " + world.getFileName()); - } catch (IOException e) { - LOG.warning("Failed to close session lock channel", e); - } - - sessionLockChannel = null; - } - } - public void launch() { fireEvent(new PageCloseEvent()); Versions.launchAndEnterWorld(profile, id, world.getFileName()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java index 7702d24596..e3c66450ca 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java @@ -130,6 +130,8 @@ public static void copyWorld(World world, Runnable runnable, FileChannel session ).whenComplete(Schedulers.javafx(), (exception) -> { if (exception == null) { resolve.run(); + System.out.println("resolve success"); + getSessionLockChannel(world, sessionLockChannel); } else { reject.accept(i18n("world.duplicate.failed")); } @@ -138,7 +140,7 @@ public static void copyWorld(World world, Runnable runnable, FileChannel session })); } - private static void closeSessionLockChannel(World world, FileChannel sessionLockChannel) throws IOException { + public static void closeSessionLockChannel(World world, FileChannel sessionLockChannel) throws IOException { if (sessionLockChannel != null) { try { sessionLockChannel.close(); @@ -148,4 +150,12 @@ private static void closeSessionLockChannel(World world, FileChannel sessionLock } } + public static void getSessionLockChannel(World world, FileChannel sessionLockChannel) throws IOException { + try { + sessionLockChannel = world.lock(); + LOG.info("Acquired lock on world " + world.getFileName()); + } catch (IOException ignored) { + } + } + } From 9864b08d4b6758bac130fed7bab8069a9620aa39 Mon Sep 17 00:00:00 2001 From: mine_ Date: Sun, 21 Dec 2025 13:26:56 +0800 Subject: [PATCH 40/66] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E4=B8=96?= =?UTF-8?q?=E7=95=8C=E9=94=81=E9=80=BB=E8=BE=91=EF=BC=8C=E5=9C=A8=E4=B8=96?= =?UTF-8?q?=E7=95=8C=E8=A2=AB=E4=BD=BF=E7=94=A8=E6=97=B6=E7=A6=81=E7=94=A8?= =?UTF-8?q?=E6=9B=B4=E5=A4=9A=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/WorldListItemSkin.java | 20 ++++++++++--- .../hmcl/ui/versions/WorldManagePage.java | 29 +++++++++---------- .../hmcl/ui/versions/WorldManageUIUtils.java | 17 +++++++---- 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java index 42f02532a6..45a3a3bdd5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java @@ -35,6 +35,7 @@ import org.jackhuang.hmcl.util.i18n.I18n; import java.time.Instant; +import java.util.stream.Stream; import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition; import static org.jackhuang.hmcl.util.StringUtils.parseColorEscapes; @@ -143,13 +144,24 @@ public void showPopupMenu(JFXPopup.PopupHPosition hPosition, double initOffsetX, } } + IconedMenuItem exportMenuItem = new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), item::export, popup); + IconedMenuItem deleteMenuItem = new IconedMenuItem(SVG.DELETE, i18n("world.delete"), item::delete, popup); + IconedMenuItem duplicateMenuItem = new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), item::copy, popup); + boolean worldLocked = world.isLocked(); + Stream.of(exportMenuItem, deleteMenuItem, duplicateMenuItem) + .forEach(iconedMenuItem -> iconedMenuItem.setDisable(worldLocked)); + popupMenu.getContent().addAll( new MenuSeparator(), - new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), item::export, popup), - new IconedMenuItem(SVG.DELETE, i18n("world.delete"), item::delete, popup), - new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), item::copy, popup), + exportMenuItem, + deleteMenuItem, + duplicateMenuItem + ); + + popupMenu.getContent().addAll( new MenuSeparator(), - new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.world"), item::reveal, popup)); + new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.world"), item::reveal, popup) + ); JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java index cc0bd6a722..e7a778495c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java @@ -41,7 +41,6 @@ import java.nio.file.Path; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; /** * @author Glavo @@ -79,8 +78,8 @@ public WorldManagePage(World world, Path backupsDir, Profile profile, String id) // Does it need to be done in the background? try { - sessionLockChannel = world.lock(); - LOG.info("Acquired lock on world " + world.getFileName()); + sessionLockChannel = WorldManageUIUtils.getSessionLockChannel(world); + System.out.println("the lock" + sessionLockChannel); } catch (IOException ignored) { } @@ -106,7 +105,7 @@ public WorldManagePage(World world, Path backupsDir, Profile profile, String id) AdvancedListBox toolbar = new AdvancedListBox(); if (world.getGameVersion() != null && world.getGameVersion().isAtLeast("1.20", "23w14a")) { - toolbar.addNavigationDrawerItem(i18n("version.launch"), SVG.ROCKET_LAUNCH, this::launch, null); + toolbar.addNavigationDrawerItem(i18n("version.launch"), SVG.ROCKET_LAUNCH, this::launch, advancedListItem -> advancedListItem.setDisable(isReadOnly())); toolbar.addNavigationDrawerItem(i18n("version.launch_script"), SVG.SCRIPT, this::generateLaunchScript, null); } @@ -142,14 +141,17 @@ public WorldManagePage(World world, Path backupsDir, Profile profile, String id) managePopupMenu.getContent().addAll( new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> WorldManageUIUtils.export(world, sessionLockChannel), managePopup), new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> WorldManageUIUtils.delete(world, () -> fireEvent(new PageCloseEvent()), sessionLockChannel), managePopup), - new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), () -> WorldManageUIUtils.copyWorld(world, null, sessionLockChannel), managePopup) + new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), () -> WorldManageUIUtils.copyWorld(world, null, sessionLockChannel, lock -> sessionLockChannel = lock), managePopup) ); toolbar.addNavigationDrawerItem(i18n("settings.game.management"), SVG.MENU, null, managePopupMenuItem -> - managePopupMenuItem.setOnAction(e -> - managePopup.show(managePopupMenuItem, - JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, - managePopupMenuItem.getWidth(), 0))); + { + managePopupMenuItem.setOnAction(e -> + managePopup.show(managePopupMenuItem, + JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, + managePopupMenuItem.getWidth(), 0)); + managePopupMenuItem.setDisable(isReadOnly()); + }); } @@ -157,17 +159,14 @@ public WorldManagePage(World world, Path backupsDir, Profile profile, String id) left.setBottom(toolbar); this.addEventHandler(Navigator.NavigationEvent.EXITED, this::onExited); - - try { - WorldManageUIUtils.getSessionLockChannel(world, sessionLockChannel); - } catch (IOException ignored) { - } this.addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated); } private void onNavigated(Navigator.NavigationEvent event) { try { - WorldManageUIUtils.getSessionLockChannel(world, sessionLockChannel); + if (sessionLockChannel == null || !sessionLockChannel.isOpen()) { + sessionLockChannel = WorldManageUIUtils.getSessionLockChannel(world); + } } catch (IOException ignored) { } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java index e3c66450ca..30f31265f9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java @@ -33,6 +33,7 @@ import java.nio.channels.FileChannel; import java.nio.file.Files; import java.nio.file.Path; +import java.util.function.Consumer; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -88,10 +89,10 @@ public static void export(World world, FileChannel sessionLockChannel) { } public static void copyWorld(World world, Runnable runnable) { - copyWorld(world, runnable, null); + copyWorld(world, runnable, null, null); } - public static void copyWorld(World world, Runnable runnable, FileChannel sessionLockChannel) { + public static void copyWorld(World world, Runnable runnable, FileChannel sessionLockChannel, Consumer lockUpdater) { Path worldPath = world.getFile(); Controllers.dialog(new InputDialogPane( i18n("world.duplicate.prompt"), @@ -131,7 +132,10 @@ public static void copyWorld(World world, Runnable runnable, FileChannel session if (exception == null) { resolve.run(); System.out.println("resolve success"); - getSessionLockChannel(world, sessionLockChannel); + FileChannel newChannel = getSessionLockChannel(world); + if (lockUpdater != null) { + lockUpdater.accept(newChannel); + } } else { reject.accept(i18n("world.duplicate.failed")); } @@ -144,17 +148,20 @@ public static void closeSessionLockChannel(World world, FileChannel sessionLockC if (sessionLockChannel != null) { try { sessionLockChannel.close(); + LOG.info("Closed session lock channel of the world " + world.getFileName()); } catch (IOException e) { throw new IOException("Failed to close session lock channel of the world " + world.getFile(), e); } } } - public static void getSessionLockChannel(World world, FileChannel sessionLockChannel) throws IOException { + public static FileChannel getSessionLockChannel(World world) throws IOException { try { - sessionLockChannel = world.lock(); + FileChannel lock = world.lock(); LOG.info("Acquired lock on world " + world.getFileName()); + return lock; } catch (IOException ignored) { + return null; } } From 742bc1970b125bfc812f21f3490ee370351f5627 Mon Sep 17 00:00:00 2001 From: mine_ Date: Sun, 21 Dec 2025 14:17:20 +0800 Subject: [PATCH 41/66] =?UTF-8?q?feat:=20=E5=8F=96=E6=B6=88=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E8=87=AA=E5=8A=A8=E6=9B=B4=E6=94=B9=E5=88=86=E8=BE=A8?= =?UTF-8?q?=E7=8E=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/WorldInfoPage.java | 41 ++++--------------- .../resources/assets/lang/I18N.properties | 8 ++-- .../resources/assets/lang/I18N_zh.properties | 8 ++-- .../assets/lang/I18N_zh_CN.properties | 8 ++-- 4 files changed, 22 insertions(+), 43 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index 435a063fcb..ca50b7d61a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -26,13 +26,11 @@ import javafx.geometry.Pos; import javafx.scene.Cursor; import javafx.scene.Node; -import javafx.scene.SnapshotParameters; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.effect.BoxBlur; import javafx.scene.image.Image; import javafx.scene.image.ImageView; -import javafx.scene.image.WritableImage; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; @@ -660,45 +658,20 @@ private void changeWorldIcon() { File file = fileChooser.showOpenDialog(Controllers.getStage()); if (file == null) return; - Image original; + Image image; try { - original = FXUtils.loadImage(file.toPath()); + image = FXUtils.loadImage(file.toPath()); } catch (Exception e) { LOG.warning("Failed to load image", e); - Controllers.dialog(i18n("world.icon.change.fail.text"), i18n("world.icon.change.fail.title"), MessageDialogPane.MessageType.ERROR); + Controllers.dialog(i18n("world.icon.change.fail.load.text"), i18n("world.icon.change.fail.load.title"), MessageDialogPane.MessageType.ERROR); return; } - Image squareImage = cropCenterSquare(original); - - Image finalImage; - if ((int) squareImage.getWidth() == 64 && (int) squareImage.getHeight() == 64) { - finalImage = squareImage; + if ((int) image.getWidth() == 64 && (int) image.getHeight() == 64) { + Path output = world.getFile().resolve("icon.png"); + saveImage(image, output); } else { - finalImage = resizeImage(squareImage, 64, 64); + Controllers.dialog(i18n("world.icon.change.fail.not_64x64.text", (int) image.getWidth(), (int) image.getHeight()), i18n("world.icon.change.fail.not_64x64.title"), MessageDialogPane.MessageType.ERROR); } - - Path output = world.getFile().resolve("icon.png"); - saveImage(finalImage, output); - } - - private Image cropCenterSquare(Image img) { - int w = (int) img.getWidth(); - int h = (int) img.getHeight(); - int size = Math.min(w, h); - int x = (w - size) / 2; - int y = (h - size) / 2; - - return new WritableImage(img.getPixelReader(), x, y, size, size); - } - - private Image resizeImage(Image img, int width, int height) { - ImageView view = new ImageView(img); - view.setFitWidth(width); - view.setFitHeight(height); - view.setPreserveRatio(false); - - SnapshotParameters params = new SnapshotParameters(); - return view.snapshot(params, null); } private void saveImage(Image image, Path path) { diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 2704e3c37f..db9b181313 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1149,10 +1149,12 @@ world.extension=World Archive world.game_version=Game Version world.icon=World Icon world.icon.change=Change world icon -world.icon.change.fail.title=Failed to parse image -world.icon.change.fail.text=This image appears to be corrupted, and HMCL cannot parse it. +world.icon.change.fail.load.title=Failed to parse image +world.icon.change.fail.load.text=This image appears to be corrupted, and HMCL cannot parse it. +world.icon.change.fail.not_64x64.title=Image size error +world.icon.change.fail.not_64x64.text=The image resolution is %d×%d instead of 64x64. Please provide a 64×64 image and try again. world.icon.change.succeed.toast=Successfully updated the world icon. -world.icon.change.tip=A 64x64 PNG image is required. HMCL will automatically crop and resize any non-conforming images to 64x64. +world.icon.change.tip=A 64×64 PNG image is required. Images with an incorrect resolution cannot be parsed by Minecraft. world.icon.choose.title=Select world icon world.import.already_exists=This world already exists. world.import.choose=Choose world archive you want to import diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index bee0c7d3c8..75a780e049 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -945,10 +945,12 @@ world.export.wizard=匯出世界「%s」 world.extension=存檔壓縮檔 world.icon=世界圖示 world.icon.change=修改世界圖示 -world.icon.change.fail.title=圖片解析失敗 -world.icon.change.fail.text=該圖片似乎已損毀,HMCL 無法解析它 +world.icon.change.fail.load.title=圖片解析失敗 +world.icon.change.fail.load.text=該圖片似乎已損毀,HMCL 無法解析它 +world.icon.change.fail.not_64x64.title=圖片大小錯誤 +world.icon.change.fail.not_64x64.text=該圖片的解析度為 %d×%d,而不是 64×64,請提供一張 64×64 解析度的圖片再次嘗試 world.icon.change.succeed.toast=世界圖示修改成功 -world.icon.change.tip=請提供一張 64x64 PNG 格式的圖片。對於不符合要求的圖片,HMCL 將自動裁剪並縮放至 64x64。 +world.icon.change.tip=請提供一張 64×64 PNG 格式的圖片。錯誤解析度的圖片將無法被Minecraft解析。 world.icon.choose.title=選擇世界圖示 world.import.already_exists=此世界已經存在 world.import.choose=選取要匯入的存檔壓縮檔 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index ee07afdf11..6ca8780418 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -956,10 +956,12 @@ world.extension=世界压缩包 world.game_version=游戏版本 world.icon=世界图标 world.icon.change=修改世界图标 -world.icon.change.fail.title=图片解析出错 -world.icon.change.fail.text=该图片似乎已损坏,HMCL 无法解析它 +world.icon.change.fail.load.title=图片解析出错 +world.icon.change.fail.load.text=该图片似乎已损坏,HMCL 无法解析它 +world.icon.change.fail.not_64x64.title=图片大小错误 +world.icon.change.fail.not_64x64.text=该图片的分辨率为 %d×%d,而不是 64×64,请提供一张 64×64 分辨率的图片再次尝试 world.icon.change.succeed.toast=世界图标修改成功 -world.icon.change.tip=请提供一张 64x64 PNG 格式的图片。对于不符合要求的图片,HMCL 将自动裁剪并缩放至 64x64。 +world.icon.change.tip=请提供一张 64×64 PNG 格式的图片。错误分辨率的图片将无法被Minecraft解析。 world.icon.choose.title=选择世界图标 world.import.already_exists=此世界已经存在 world.import.choose=选择要导入的存档压缩包 From 03a999b1c721f90a4762a9e73f819c07b0d3bc92 Mon Sep 17 00:00:00 2001 From: mine_ Date: Sun, 21 Dec 2025 14:43:23 +0800 Subject: [PATCH 42/66] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=B8=80?= =?UTF-8?q?=E4=BA=9B=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/ui/versions/WorldManagePage.java | 1 - .../jackhuang/hmcl/ui/versions/WorldManageUIUtils.java | 9 ++++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java index e7a778495c..3c20fea3f0 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java @@ -79,7 +79,6 @@ public WorldManagePage(World world, Path backupsDir, Profile profile, String id) // Does it need to be done in the background? try { sessionLockChannel = WorldManageUIUtils.getSessionLockChannel(world); - System.out.println("the lock" + sessionLockChannel); } catch (IOException ignored) { } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java index 30f31265f9..a00096c13c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java @@ -131,14 +131,13 @@ public static void copyWorld(World world, Runnable runnable, FileChannel session ).whenComplete(Schedulers.javafx(), (exception) -> { if (exception == null) { resolve.run(); - System.out.println("resolve success"); - FileChannel newChannel = getSessionLockChannel(world); - if (lockUpdater != null) { - lockUpdater.accept(newChannel); - } } else { reject.accept(i18n("world.duplicate.failed")); } + FileChannel newChannel = getSessionLockChannel(world); + if (lockUpdater != null) { + lockUpdater.accept(newChannel); + } }) .start(); })); From d3efcc0c0764d669ea2b44db99ab1b400877346a Mon Sep 17 00:00:00 2001 From: mine_ Date: Mon, 22 Dec 2025 13:39:03 +0800 Subject: [PATCH 43/66] =?UTF-8?q?feat:=20=E4=BF=AE=E5=A4=8D=E6=97=A0?= =?UTF-8?q?=E6=B3=95=E8=8E=B7=E5=8F=96=E6=98=AF=E5=90=A6=E4=B8=BA=E6=94=BE?= =?UTF-8?q?=E5=A4=A7=E5=8C=96=E4=B8=96=E7=95=8C=E7=9A=84=E9=97=AE=E9=A2=98?= =?UTF-8?q?=EF=BC=8C=E7=8E=B0=E5=9C=A8=E8=8E=B7=E5=8F=96leveldat=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E5=85=B1=E4=BA=ABleveldat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/WorldInfoPage.java | 7 +- .../java/org/jackhuang/hmcl/game/World.java | 78 ++++++++++--------- 2 files changed, 45 insertions(+), 40 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index ca50b7d61a..da78989e51 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -92,7 +92,7 @@ private CompoundTag loadWorldInfo() throws IOException { if (!Files.isDirectory(world.getFile())) throw new IOException("Not a valid world directory"); - return world.readLevelDat(); + return world.getLevelData(); } private void updateControls() { @@ -191,10 +191,7 @@ private void updateControls() { FXUtils.copyOnDoubleClick(seedLabel); BorderPane.setAlignment(seedLabel, Pos.CENTER_RIGHT); - Tag tag = worldGenSettings != null ? worldGenSettings.get("seed") : dataTag.get("RandomSeed"); - if (tag instanceof LongTag) { - seedLabel.setText(tag.getValue().toString()); - } + seedLabel.setText(world.getSeed() != null ? world.getSeed().toString() : ""); BoxBlur blur = new BoxBlur(); blur.setIterations(3); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java index a6e9b3e62c..ae9e195ede 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java @@ -18,10 +18,7 @@ package org.jackhuang.hmcl.game; import com.github.steveice10.opennbt.NBTIO; -import com.github.steveice10.opennbt.tag.builtin.CompoundTag; -import com.github.steveice10.opennbt.tag.builtin.LongTag; -import com.github.steveice10.opennbt.tag.builtin.StringTag; -import com.github.steveice10.opennbt.tag.builtin.Tag; +import com.github.steveice10.opennbt.tag.builtin.*; import javafx.scene.image.Image; import org.jackhuang.hmcl.util.io.*; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; @@ -50,6 +47,7 @@ public final class World { private String fileName; private String worldName; private GameVersionNumber gameVersion; + private CompoundTag levelData; private long lastPlayed; private Image icon; private Long seed; @@ -67,24 +65,6 @@ else if (Files.isRegularFile(file)) throw new IOException("Path " + file + " cannot be recognized as a Minecraft world"); } - private void loadFromDirectory() throws IOException { - fileName = FileUtils.getName(file); - Path levelDat = file.resolve("level.dat"); - loadWorldInfo(levelDat); - isLocked = isLocked(getSessionLockFile()); - - Path iconFile = file.resolve("icon.png"); - if (Files.isRegularFile(iconFile)) { - try (InputStream inputStream = Files.newInputStream(iconFile)) { - icon = new Image(inputStream, 64, 64, true, false); - if (icon.isError()) - throw icon.getException(); - } catch (Exception e) { - LOG.warning("Failed to load world icon", e); - } - } - } - public Path getFile() { return file; } @@ -109,6 +89,10 @@ public Path getSessionLockFile() { return file.resolve("session.lock"); } + public CompoundTag getLevelData() { + return levelData; + } + public long getLastPlayed() { return lastPlayed; } @@ -133,6 +117,24 @@ public boolean isLocked() { return isLocked; } + private void loadFromDirectory() throws IOException { + fileName = FileUtils.getName(file); + Path levelDat = file.resolve("level.dat"); + loadWorldInfo(levelDat); + isLocked = isLocked(getSessionLockFile()); + + Path iconFile = file.resolve("icon.png"); + if (Files.isRegularFile(iconFile)) { + try (InputStream inputStream = Files.newInputStream(iconFile)) { + icon = new Image(inputStream, 64, 64, true, false); + if (icon.isError()) + throw icon.getException(); + } catch (Exception e) { + LOG.warning("Failed to load world icon", e); + } + } + } + private void loadFromZipImpl(Path root) throws IOException { Path levelDat = root.resolve("level.dat"); if (!Files.exists(levelDat)) @@ -173,6 +175,7 @@ private void loadFromZip() throws IOException { private void loadWorldInfo(Path levelDat) throws IOException { CompoundTag nbt = parseLevelDat(levelDat); + levelData = nbt; CompoundTag data = nbt.get("Data"); if (data == null) @@ -210,12 +213,25 @@ private void loadWorldInfo(Path levelDat) throws IOException { } } - // FIXME: Only work for 1.15 and below - if (data.get("generatorName") instanceof StringTag) { + if (data.get("generatorName") instanceof StringTag) { //Valid before 1.16 largeBiomes = "largeBiomes".equals(data.get("generatorName").getValue()); } else { - largeBiomes = false; + if (data.get("WorldGenSettings") instanceof CompoundTag worldGenSettingsTag + && worldGenSettingsTag.get("dimensions") instanceof CompoundTag dimensionsTag + && dimensionsTag.get("minecraft:overworld") instanceof CompoundTag overworldTag + && overworldTag.get("generator") instanceof CompoundTag generatorTag) { + if (generatorTag.get("settings") instanceof StringTag settingsTag) { //Valid after 1.16.2 + largeBiomes = "minecraft:large_biomes".equals(settingsTag.getValue()); + } + if (generatorTag.get("biome_source") instanceof CompoundTag biomeSourceTag + && biomeSourceTag.get("large_biomes") instanceof ByteTag largeBiomesTag) { //Valid between 1.16 and 1.16.2 + largeBiomes = largeBiomesTag.getValue() == (byte) 1; + } + } else { + largeBiomes = false; + } } + System.out.println("largeBiomes: " + largeBiomes); } public void rename(String newName) throws IOException { @@ -223,10 +239,9 @@ public void rename(String newName) throws IOException { throw new IOException("Not a valid world directory"); // Change the name recorded in level.dat - CompoundTag nbt = readLevelDat(); - CompoundTag data = nbt.get("Data"); + CompoundTag data = levelData.get("Data"); data.put(new StringTag("LevelName", newName)); - writeLevelDat(nbt); + writeLevelDat(levelData); // then change the folder's name Files.move(file, file.resolveSibling(newName)); @@ -297,13 +312,6 @@ public void copy(String newName) throws IOException { newWorld.rename(newName); } - public CompoundTag readLevelDat() throws IOException { - if (!Files.isDirectory(file)) - throw new IOException("Not a valid world directory"); - - return parseLevelDat(getLevelDatFile()); - } - public FileChannel lock() throws WorldLockedException { Path lockFile = getSessionLockFile(); FileChannel channel = null; From 375ac4dea41c53d1540d8559487d4338690c594d Mon Sep 17 00:00:00 2001 From: mine_ Date: Mon, 22 Dec 2025 18:47:29 +0800 Subject: [PATCH 44/66] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=B8=80?= =?UTF-8?q?=E4=BA=9B=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java index ae9e195ede..05e03fa0b6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java @@ -220,18 +220,18 @@ private void loadWorldInfo(Path levelDat) throws IOException { && worldGenSettingsTag.get("dimensions") instanceof CompoundTag dimensionsTag && dimensionsTag.get("minecraft:overworld") instanceof CompoundTag overworldTag && overworldTag.get("generator") instanceof CompoundTag generatorTag) { - if (generatorTag.get("settings") instanceof StringTag settingsTag) { //Valid after 1.16.2 - largeBiomes = "minecraft:large_biomes".equals(settingsTag.getValue()); - } if (generatorTag.get("biome_source") instanceof CompoundTag biomeSourceTag && biomeSourceTag.get("large_biomes") instanceof ByteTag largeBiomesTag) { //Valid between 1.16 and 1.16.2 largeBiomes = largeBiomesTag.getValue() == (byte) 1; + } else if (generatorTag.get("settings") instanceof StringTag settingsTag) { //Valid after 1.16.2 + largeBiomes = "minecraft:large_biomes".equals(settingsTag.getValue()); + } else { + largeBiomes = false; } } else { largeBiomes = false; } } - System.out.println("largeBiomes: " + largeBiomes); } public void rename(String newName) throws IOException { From 238a1cbf542371c1e5b77650e28687a7176a76f4 Mon Sep 17 00:00:00 2001 From: mine_ Date: Tue, 23 Dec 2025 10:21:41 +0800 Subject: [PATCH 45/66] =?UTF-8?q?fix:=20=E5=BA=94=E7=94=A8copilot=E7=9A=84?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=BB=BA=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/versions/WorldInfoPage.java | 4 ++-- .../jackhuang/hmcl/ui/versions/WorldManagePage.java | 12 +++--------- .../hmcl/ui/versions/WorldManageUIUtils.java | 2 +- .../src/main/java/org/jackhuang/hmcl/game/World.java | 7 ++++++- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index da78989e51..cc1f56c99b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -340,7 +340,7 @@ private void updateControls() { setRightTextLabel(spawnPane, spawnLabel, () -> { Dimension dimension; - if (player.get("respawn") instanceof CompoundTag respawnTag) { //Valid after 25w07a + if (player.get("respawn") instanceof CompoundTag respawnTag && respawnTag.get("dimension") != null) { //Valid after 25w07a dimension = Dimension.of(respawnTag.get("dimension")); Tag posTag = respawnTag.get("pos"); @@ -462,7 +462,7 @@ private void setLeftLabel(BorderPane borderPane, @PropertyKey(resourceBundle = " } private void setRightTextField(BorderPane borderPane, JFXTextField textField, int perfWidth) { - textField.setDisable(worldManagePage.isDisable()); + textField.setDisable(worldManagePage.isReadOnly()); textField.setPrefWidth(perfWidth); textField.setAlignment(Pos.CENTER_RIGHT); borderPane.setRight(textField); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java index 3c20fea3f0..7885bf629d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java @@ -77,10 +77,7 @@ public WorldManagePage(World world, Path backupsDir, Profile profile, String id) header.select(worldInfoTab); // Does it need to be done in the background? - try { - sessionLockChannel = WorldManageUIUtils.getSessionLockChannel(world); - } catch (IOException ignored) { - } + sessionLockChannel = WorldManageUIUtils.getSessionLockChannel(world); setCenter(transitionPane); @@ -162,11 +159,8 @@ public WorldManagePage(World world, Path backupsDir, Profile profile, String id) } private void onNavigated(Navigator.NavigationEvent event) { - try { - if (sessionLockChannel == null || !sessionLockChannel.isOpen()) { - sessionLockChannel = WorldManageUIUtils.getSessionLockChannel(world); - } - } catch (IOException ignored) { + if (sessionLockChannel == null || !sessionLockChannel.isOpen()) { + sessionLockChannel = WorldManageUIUtils.getSessionLockChannel(world); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java index a00096c13c..17390d6178 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java @@ -154,7 +154,7 @@ public static void closeSessionLockChannel(World world, FileChannel sessionLockC } } - public static FileChannel getSessionLockChannel(World world) throws IOException { + public static FileChannel getSessionLockChannel(World world) { try { FileChannel lock = world.lock(); LOG.info("Acquired lock on world " + world.getFileName()); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java index 05e03fa0b6..67c63cb5ac 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java @@ -303,8 +303,13 @@ public void delete() throws IOException { } public void copy(String newName) throws IOException { - if (!Files.isDirectory(file)) + if (!Files.isDirectory(file)) { throw new IOException(); + } + + if (isLocked()) { + throw new WorldLockedException("The world " + getFile() + " has been locked"); + } Path newPath = file.resolveSibling(newName); FileUtils.copyDirectory(file, newPath); From a1e5d625e16bea1a852f901df83f500138b1cdd3 Mon Sep 17 00:00:00 2001 From: mine_ Date: Tue, 23 Dec 2025 10:46:54 +0800 Subject: [PATCH 46/66] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=A7=8D?= =?UTF-8?q?=E5=AD=90=E6=98=BE=E7=A4=BA=E5=9B=BE=E6=A0=87=E5=92=8C=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E4=B8=96=E7=95=8C=E5=9B=BE=E6=A0=87=E5=9B=BE=E6=A0=87?= =?UTF-8?q?=E7=9A=84=E4=B8=80=E4=BA=9B=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/WorldInfoPage.java | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index cc1f56c99b..1b5212625e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -25,7 +25,6 @@ import javafx.collections.ObservableList; import javafx.geometry.Pos; import javafx.scene.Cursor; -import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.effect.BoxBlur; @@ -149,26 +148,32 @@ private void updateControls() { { setLeftLabel(iconPane, "world.icon"); - FXUtils.limitSize(iconImageView, 32, 32); - iconImageView.setImage(world.getIcon() == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : world.getIcon()); - iconImageView.setCursor(Cursor.HAND); - iconImageView.setDisable(worldManagePage.isReadOnly()); - - Node editIcon = SVG.EDIT.createIcon(12); - editIcon.setDisable(worldManagePage.isReadOnly()); - editIcon.setCursor(Cursor.HAND); Runnable onClickAction = () -> Controllers.confirm( i18n("world.icon.change.tip"), i18n("world.icon.change"), MessageDialogPane.MessageType.INFO, this::changeWorldIcon, null ); - FXUtils.onClicked(editIcon, onClickAction); - FXUtils.onClicked(iconImageView, onClickAction); - FXUtils.installFastTooltip(editIcon, i18n("world.icon.change")); + + FXUtils.limitSize(iconImageView, 32, 32); + { + iconImageView.setImage(world.getIcon() == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : world.getIcon()); + } + + StackPane editIconButton = new StackPane(); + { + editIconButton.setCursor(Cursor.HAND); + editIconButton.setAlignment(Pos.CENTER_LEFT); + FXUtils.setLimitWidth(editIconButton, 12); + FXUtils.setLimitHeight(editIconButton, 12); + editIconButton.getChildren().setAll(SVG.EDIT.createIcon(12)); + editIconButton.setDisable(worldManagePage.isReadOnly()); + FXUtils.onClicked(editIconButton, onClickAction); + FXUtils.installFastTooltip(editIconButton, i18n("world.icon.change")); + } HBox hBox = new HBox(8); hBox.setAlignment(Pos.CENTER_LEFT); - hBox.getChildren().addAll(editIcon, iconImageView); + hBox.getChildren().addAll(editIconButton, iconImageView); iconPane.setRight(hBox); } @@ -181,6 +186,7 @@ private void updateControls() { StackPane visibilityButton = new StackPane(); { visibilityButton.setCursor(Cursor.HAND); + visibilityButton.setAlignment(Pos.BOTTOM_RIGHT); FXUtils.setLimitWidth(visibilityButton, 12); FXUtils.setLimitHeight(visibilityButton, 12); FXUtils.onClicked(visibilityButton, () -> visibility.set(!visibility.get())); @@ -189,7 +195,7 @@ private void updateControls() { Label seedLabel = new Label(); { FXUtils.copyOnDoubleClick(seedLabel); - BorderPane.setAlignment(seedLabel, Pos.CENTER_RIGHT); + seedLabel.setAlignment(Pos.CENTER_RIGHT); seedLabel.setText(world.getSeed() != null ? world.getSeed().toString() : ""); From 7641e2aa86b914c6b4ff529696bd1f34366d6768 Mon Sep 17 00:00:00 2001 From: mine_ Date: Thu, 25 Dec 2025 11:22:39 +0800 Subject: [PATCH 47/66] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/jackhuang/hmcl/game/World.java | 42 +++++++------------ 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java index 67c63cb5ac..4b555f0eb6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java @@ -174,47 +174,35 @@ private void loadFromZip() throws IOException { } private void loadWorldInfo(Path levelDat) throws IOException { - CompoundTag nbt = parseLevelDat(levelDat); - levelData = nbt; - - CompoundTag data = nbt.get("Data"); + this.levelData = parseLevelDat(levelDat); + CompoundTag data = levelData.get("Data"); if (data == null) throw new IOException("level.dat missing Data"); - if (data.get("LevelName") instanceof StringTag) - worldName = data.get("LevelName").getValue(); + if (data.get("LevelName") instanceof StringTag levelNameTag) + worldName = levelNameTag.getValue(); else throw new IOException("level.dat missing LevelName"); - if (data.get("LastPlayed") instanceof LongTag) - lastPlayed = data.get("LastPlayed").getValue(); + if (data.get("LastPlayed") instanceof LongTag lastPlayedTag) + lastPlayed = lastPlayedTag.getValue(); else throw new IOException("level.dat missing LastPlayed"); gameVersion = null; - if (data.get("Version") instanceof CompoundTag) { - CompoundTag version = data.get("Version"); - - if (version.get("Name") instanceof StringTag) - gameVersion = GameVersionNumber.asGameVersion(version.get("Name").getValue()); + if (data.get("Version") instanceof CompoundTag versionTag && versionTag.get("Name") instanceof StringTag nameTag) { + gameVersion = GameVersionNumber.asGameVersion(nameTag.getValue()); } - Tag worldGenSettings = data.get("WorldGenSettings"); - if (worldGenSettings instanceof CompoundTag) { - Tag seedTag = ((CompoundTag) worldGenSettings).get("seed"); - if (seedTag instanceof LongTag) { - seed = ((LongTag) seedTag).getValue(); - } - } - if (seed == null) { - Tag seedTag = data.get("RandomSeed"); - if (seedTag instanceof LongTag) { - seed = ((LongTag) seedTag).getValue(); - } + if (data.get("WorldGenSettings") instanceof CompoundTag worldGenSettingsTag && worldGenSettingsTag.get("seed") instanceof LongTag seedTag) { //Valid after 1.16 + seed = seedTag.getValue(); + } else if (data.get("RandomSeed") instanceof LongTag seedTag) { //Valid before 1.16 + seed = seedTag.getValue(); } - if (data.get("generatorName") instanceof StringTag) { //Valid before 1.16 - largeBiomes = "largeBiomes".equals(data.get("generatorName").getValue()); + + if (data.get("generatorName") instanceof StringTag generatorNameTag) { //Valid before 1.16 + largeBiomes = "largeBiomes".equals(generatorNameTag.getValue()); } else { if (data.get("WorldGenSettings") instanceof CompoundTag worldGenSettingsTag && worldGenSettingsTag.get("dimensions") instanceof CompoundTag dimensionsTag From 8578cfa578960374018f29636810d699f9046bf5 Mon Sep 17 00:00:00 2001 From: mine_ Date: Sun, 28 Dec 2025 15:18:58 +0800 Subject: [PATCH 48/66] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D20w14infinite?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E7=9A=84=E4=B8=96=E7=95=8C=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E8=A2=AB=E6=AD=A3=E7=A1=AE=E5=8A=A0=E8=BD=BD=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/org/jackhuang/hmcl/game/World.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java index 4b555f0eb6..a75acbeb23 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java @@ -120,6 +120,9 @@ public boolean isLocked() { private void loadFromDirectory() throws IOException { fileName = FileUtils.getName(file); Path levelDat = file.resolve("level.dat"); + if (!Files.exists(levelDat)) { // version 20w14infinite + levelDat = file.resolve("special_level.dat"); + } loadWorldInfo(levelDat); isLocked = isLocked(getSessionLockFile()); @@ -137,8 +140,12 @@ private void loadFromDirectory() throws IOException { private void loadFromZipImpl(Path root) throws IOException { Path levelDat = root.resolve("level.dat"); - if (!Files.exists(levelDat)) - throw new IOException("Not a valid world zip file since level.dat cannot be found."); + if (!Files.exists(levelDat)) { //version 20w14infinite + levelDat = root.resolve("special_level.dat"); + } + if (!Files.exists(levelDat)) { + throw new IOException("Not a valid world zip file since level.dat or special_level.dat cannot be found."); + } loadWorldInfo(levelDat); From a99902c4b1ccc14fc971b47ec890522281ba36a7 Mon Sep 17 00:00:00 2001 From: mine_ Date: Thu, 1 Jan 2026 21:59:24 +0800 Subject: [PATCH 49/66] =?UTF-8?q?feat:=20World=E4=B8=AD=E4=BF=9D=E5=AD=98?= =?UTF-8?q?=E7=9A=84=E9=83=A8=E5=88=86=E5=AD=97=E6=AE=B5=E5=B0=86=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E4=BF=9D=E5=AD=98=E8=80=8C=E6=98=AFget=E6=97=B6?= =?UTF-8?q?=E8=8E=B7=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/WorldInfoPage.java | 1 - .../java/org/jackhuang/hmcl/game/World.java | 94 +++++++++---------- 2 files changed, 44 insertions(+), 51 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index 1b5212625e..7a596dd022 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -127,7 +127,6 @@ private void updateControls() { try { stringTag.setValue(newValue); world.setWorldName(newValue); - saveLevelDat(); } catch (Throwable ignored) { } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java index a75acbeb23..98f87890d3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java @@ -45,13 +45,8 @@ public final class World { private final Path file; private String fileName; - private String worldName; - private GameVersionNumber gameVersion; private CompoundTag levelData; - private long lastPlayed; private Image icon; - private Long seed; - private boolean largeBiomes; private boolean isLocked; public World(Path file) throws IOException { @@ -74,11 +69,17 @@ public String getFileName() { } public String getWorldName() { - return worldName; + CompoundTag data = levelData.get("Data"); + StringTag levelNameTag = data.get("LevelName"); + return levelNameTag.getValue(); } - public void setWorldName(String worldName) { - this.worldName = worldName; + public void setWorldName(String worldName) throws IOException { + + if (levelData.get("Data") instanceof CompoundTag data && data.get("LevelName") instanceof StringTag levelNameTag) { + levelNameTag.setValue(worldName); + writeLevelDat(levelData); + } } public Path getLevelDatFile() { @@ -94,19 +95,48 @@ public CompoundTag getLevelData() { } public long getLastPlayed() { - return lastPlayed; + CompoundTag data = levelData.get("Data"); + LongTag lastPlayedTag = data.get("LastPlayed"); + return lastPlayedTag.getValue(); } public @Nullable GameVersionNumber getGameVersion() { - return gameVersion; + if (levelData.get("Data") instanceof CompoundTag data && + data.get("Version") instanceof CompoundTag versionTag && + versionTag.get("Name") instanceof StringTag nameTag) { + return GameVersionNumber.asGameVersion(nameTag.getValue()); + } + return null; } public @Nullable Long getSeed() { - return seed; + CompoundTag data = levelData.get("Data"); + if (data.get("WorldGenSettings") instanceof CompoundTag worldGenSettingsTag && worldGenSettingsTag.get("seed") instanceof LongTag seedTag) { //Valid after 1.16 + return seedTag.getValue(); + } else if (data.get("RandomSeed") instanceof LongTag seedTag) { //Valid before 1.16 + return seedTag.getValue(); + } + return null; } public boolean isLargeBiomes() { - return largeBiomes; + CompoundTag data = levelData.get("Data"); + if (data.get("generatorName") instanceof StringTag generatorNameTag) { //Valid before 1.16 + return "largeBiomes".equals(generatorNameTag.getValue()); + } else { + if (data.get("WorldGenSettings") instanceof CompoundTag worldGenSettingsTag + && worldGenSettingsTag.get("dimensions") instanceof CompoundTag dimensionsTag + && dimensionsTag.get("minecraft:overworld") instanceof CompoundTag overworldTag + && overworldTag.get("generator") instanceof CompoundTag generatorTag) { + if (generatorTag.get("biome_source") instanceof CompoundTag biomeSourceTag + && biomeSourceTag.get("large_biomes") instanceof ByteTag largeBiomesTag) { //Valid between 1.16 and 1.16.2 + return largeBiomesTag.getValue() == (byte) 1; + } else if (generatorTag.get("settings") instanceof StringTag settingsTag) { //Valid after 1.16.2 + return "minecraft:large_biomes".equals(settingsTag.getValue()); + } + } + return false; + } } public Image getIcon() { @@ -186,47 +216,11 @@ private void loadWorldInfo(Path levelDat) throws IOException { if (data == null) throw new IOException("level.dat missing Data"); - if (data.get("LevelName") instanceof StringTag levelNameTag) - worldName = levelNameTag.getValue(); - else + if (!(data.get("LevelName") instanceof StringTag)) throw new IOException("level.dat missing LevelName"); - if (data.get("LastPlayed") instanceof LongTag lastPlayedTag) - lastPlayed = lastPlayedTag.getValue(); - else + if (!(data.get("LastPlayed") instanceof LongTag)) throw new IOException("level.dat missing LastPlayed"); - - gameVersion = null; - if (data.get("Version") instanceof CompoundTag versionTag && versionTag.get("Name") instanceof StringTag nameTag) { - gameVersion = GameVersionNumber.asGameVersion(nameTag.getValue()); - } - - if (data.get("WorldGenSettings") instanceof CompoundTag worldGenSettingsTag && worldGenSettingsTag.get("seed") instanceof LongTag seedTag) { //Valid after 1.16 - seed = seedTag.getValue(); - } else if (data.get("RandomSeed") instanceof LongTag seedTag) { //Valid before 1.16 - seed = seedTag.getValue(); - } - - - if (data.get("generatorName") instanceof StringTag generatorNameTag) { //Valid before 1.16 - largeBiomes = "largeBiomes".equals(generatorNameTag.getValue()); - } else { - if (data.get("WorldGenSettings") instanceof CompoundTag worldGenSettingsTag - && worldGenSettingsTag.get("dimensions") instanceof CompoundTag dimensionsTag - && dimensionsTag.get("minecraft:overworld") instanceof CompoundTag overworldTag - && overworldTag.get("generator") instanceof CompoundTag generatorTag) { - if (generatorTag.get("biome_source") instanceof CompoundTag biomeSourceTag - && biomeSourceTag.get("large_biomes") instanceof ByteTag largeBiomesTag) { //Valid between 1.16 and 1.16.2 - largeBiomes = largeBiomesTag.getValue() == (byte) 1; - } else if (generatorTag.get("settings") instanceof StringTag settingsTag) { //Valid after 1.16.2 - largeBiomes = "minecraft:large_biomes".equals(settingsTag.getValue()); - } else { - largeBiomes = false; - } - } else { - largeBiomes = false; - } - } } public void rename(String newName) throws IOException { From 26d6163e215f8bec4a5709256041bc7e9bfb2d00 Mon Sep 17 00:00:00 2001 From: mine_ Date: Thu, 1 Jan 2026 22:20:51 +0800 Subject: [PATCH 50/66] fix some issue --- .../jackhuang/hmcl/ui/versions/WorldInfoPage.java | 13 ++++++------- .../main/java/org/jackhuang/hmcl/game/World.java | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index 7a596dd022..7bc597371e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -125,7 +125,6 @@ private void updateControls() { worldNameField.textProperty().addListener((observable, oldValue, newValue) -> { if (newValue != null) { try { - stringTag.setValue(newValue); world.setWorldName(newValue); } catch (Throwable ignored) { } @@ -349,7 +348,7 @@ private void updateControls() { dimension = Dimension.of(respawnTag.get("dimension")); Tag posTag = respawnTag.get("pos"); - if (posTag instanceof IntArrayTag intArrayTag) { + if (posTag instanceof IntArrayTag intArrayTag && intArrayTag.length() >= 3) { return dimension.formatPosition(intArrayTag.getValue(0), intArrayTag.getValue(1), intArrayTag.getValue(2)); } } else if (player.get("SpawnX") instanceof IntTag intX @@ -417,7 +416,7 @@ private void updateControls() { Tag tag = player.get("Health"); if (tag instanceof FloatTag floatTag) { - setTagAndTextFiled(floatTag, healthField); + setTagAndTextField(floatTag, healthField); } else { healthField.setDisable(true); } @@ -431,7 +430,7 @@ private void updateControls() { Tag tag = player.get("foodLevel"); if (tag instanceof IntTag intTag) { - setTagAndTextFiled(intTag, foodLevelField); + setTagAndTextField(intTag, foodLevelField); } else { foodLevelField.setDisable(true); } @@ -445,7 +444,7 @@ private void updateControls() { Tag tag = player.get("XpLevel"); if (tag instanceof IntTag intTag) { - setTagAndTextFiled(intTag, xpLevelField); + setTagAndTextField(intTag, xpLevelField); } else { xpLevelField.setDisable(true); } @@ -506,7 +505,7 @@ private void checkTagAndSetListener(Tag tag, OptionToggleButton toggleButton) { } } - private void setTagAndTextFiled(IntTag intTag, JFXTextField jfxTextField) { + private void setTagAndTextField(IntTag intTag, JFXTextField jfxTextField) { jfxTextField.setText(String.valueOf(intTag.getValue())); jfxTextField.textProperty().addListener((o, oldValue, newValue) -> { @@ -524,7 +523,7 @@ private void setTagAndTextFiled(IntTag intTag, JFXTextField jfxTextField) { jfxTextField.setValidators(new NumberValidator(i18n("input.number"), true)); } - private void setTagAndTextFiled(FloatTag floatTag, JFXTextField jfxTextField) { + private void setTagAndTextField(FloatTag floatTag, JFXTextField jfxTextField) { jfxTextField.setText(new DecimalFormat("#").format(floatTag.getValue().floatValue())); jfxTextField.textProperty().addListener((o, oldValue, newValue) -> { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java index 98f87890d3..ce74144cd4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java @@ -293,7 +293,7 @@ public void delete() throws IOException { public void copy(String newName) throws IOException { if (!Files.isDirectory(file)) { - throw new IOException(); + throw new IOException("Not a valid world directory"); } if (isLocked()) { From 123dcd95bc89df2c02aff8983dde7b37ecdb19bf Mon Sep 17 00:00:00 2001 From: mine_ Date: Thu, 1 Jan 2026 22:24:09 +0800 Subject: [PATCH 51/66] fix some issue --- .../main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index 7bc597371e..d75e648405 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -676,12 +676,14 @@ private void changeWorldIcon() { } private void saveImage(Image image, Path path) { + Image oldImage = iconImageView.getImage(); try { PNGJavaFXUtils.writeImage(image, path); iconImageView.setImage(image); Controllers.showToast(i18n("world.icon.change.succeed.toast")); } catch (IOException e) { LOG.warning(e.getMessage()); + iconImageView.setImage(oldImage); } } } From afa7ad7404a7d22215766be4803d6e512765b8ed Mon Sep 17 00:00:00 2001 From: mine_ Date: Fri, 2 Jan 2026 16:40:17 +0800 Subject: [PATCH 52/66] feat: update --- .../java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java | 3 ++- HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index d75e648405..2c8a0350ca 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -126,7 +126,8 @@ private void updateControls() { if (newValue != null) { try { world.setWorldName(newValue); - } catch (Throwable ignored) { + } catch (Exception e) { + LOG.warning("Failed to set world name", e); } } }); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java index ce74144cd4..8c3447d79e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java @@ -75,7 +75,6 @@ public String getWorldName() { } public void setWorldName(String worldName) throws IOException { - if (levelData.get("Data") instanceof CompoundTag data && data.get("LevelName") instanceof StringTag levelNameTag) { levelNameTag.setValue(worldName); writeLevelDat(levelData); From 3da183a5f540590892867a2674c7b791f622cc56 Mon Sep 17 00:00:00 2001 From: mine_ Date: Fri, 2 Jan 2026 17:08:25 +0800 Subject: [PATCH 53/66] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=EF=BC=8C=E5=A4=8D=E5=88=B6=E4=B8=96=E7=95=8C=E6=97=B6?= =?UTF-8?q?=E8=B7=B3=E8=BF=87session.lock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/WorldManagePage.java | 2 +- .../hmcl/ui/versions/WorldManageUIUtils.java | 16 ---------------- .../main/java/org/jackhuang/hmcl/game/World.java | 2 +- 3 files changed, 2 insertions(+), 18 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java index 7885bf629d..fbe1c7ac54 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java @@ -137,7 +137,7 @@ public WorldManagePage(World world, Path backupsDir, Profile profile, String id) managePopupMenu.getContent().addAll( new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> WorldManageUIUtils.export(world, sessionLockChannel), managePopup), new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> WorldManageUIUtils.delete(world, () -> fireEvent(new PageCloseEvent()), sessionLockChannel), managePopup), - new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), () -> WorldManageUIUtils.copyWorld(world, null, sessionLockChannel, lock -> sessionLockChannel = lock), managePopup) + new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), () -> WorldManageUIUtils.copyWorld(world, null), managePopup) ); toolbar.addNavigationDrawerItem(i18n("settings.game.management"), SVG.MENU, null, managePopupMenuItem -> diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java index 17390d6178..b70095d767 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java @@ -33,7 +33,6 @@ import java.nio.channels.FileChannel; import java.nio.file.Files; import java.nio.file.Path; -import java.util.function.Consumer; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -89,10 +88,6 @@ public static void export(World world, FileChannel sessionLockChannel) { } public static void copyWorld(World world, Runnable runnable) { - copyWorld(world, runnable, null, null); - } - - public static void copyWorld(World world, Runnable runnable, FileChannel sessionLockChannel, Consumer lockUpdater) { Path worldPath = world.getFile(); Controllers.dialog(new InputDialogPane( i18n("world.duplicate.prompt"), @@ -114,13 +109,6 @@ public static void copyWorld(World world, Runnable runnable, FileChannel session return; } - try { - closeSessionLockChannel(world, sessionLockChannel); - } catch (IOException e) { - Controllers.dialog(i18n("world.locked.failed"), null, MessageDialogPane.MessageType.WARNING); - LOG.warning("unlock world fail", e); - } - Task.runAsync(Schedulers.io(), () -> world.copy(result)) .thenAcceptAsync(Schedulers.javafx(), (Void) -> Controllers.showToast(i18n("world.duplicate.success.toast"))) .thenAcceptAsync(Schedulers.javafx(), (Void) -> { @@ -134,10 +122,6 @@ public static void copyWorld(World world, Runnable runnable, FileChannel session } else { reject.accept(i18n("world.duplicate.failed")); } - FileChannel newChannel = getSessionLockChannel(world); - if (lockUpdater != null) { - lockUpdater.accept(newChannel); - } }) .start(); })); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java index 8c3447d79e..26d07689ed 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java @@ -300,7 +300,7 @@ public void copy(String newName) throws IOException { } Path newPath = file.resolveSibling(newName); - FileUtils.copyDirectory(file, newPath); + FileUtils.copyDirectory(file, newPath, path -> !path.contains("session.lock")); World newWorld = new World(newPath); newWorld.rename(newName); } From 29daf82f3eef4d5e1c0a2d0f27a67453b560f5a6 Mon Sep 17 00:00:00 2001 From: mine_ Date: Fri, 2 Jan 2026 17:35:48 +0800 Subject: [PATCH 54/66] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java | 2 +- .../org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index 2c8a0350ca..af7dc2fffd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -140,7 +140,7 @@ private void updateControls() { { setLeftLabel(gameVersionPane, "world.info.game_version"); Label gameVersionLabel = new Label(); - setRightTextLabel(gameVersionPane, gameVersionLabel, () -> world.getGameVersion().toNormalizedString()); + setRightTextLabel(gameVersionPane, gameVersionLabel, () -> world.getGameVersion() == null ? "" : world.getGameVersion().toNormalizedString()); } BorderPane iconPane = new BorderPane(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java index b70095d767..761144ce4d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java @@ -116,11 +116,12 @@ public static void copyWorld(World world, Runnable runnable) { runnable.run(); } } - ).whenComplete(Schedulers.javafx(), (exception) -> { - if (exception == null) { + ).whenComplete(Schedulers.javafx(), (throwable) -> { + if (throwable == null) { resolve.run(); } else { reject.accept(i18n("world.duplicate.failed")); + LOG.warning("Failed to duplicate world " + world.getFile(), throwable); } }) .start(); From 9723e1a639ada62eed8d4fb7696acb364e53d222 Mon Sep 17 00:00:00 2001 From: mine_ Date: Fri, 2 Jan 2026 21:23:21 +0800 Subject: [PATCH 55/66] =?UTF-8?q?feat:=20level.dat=E7=8E=B0=E5=9C=A8?= =?UTF-8?q?=E4=BC=9A=E5=9C=A8=E5=8A=A0=E8=BD=BD=E4=B8=96=E7=95=8C=E6=97=B6?= =?UTF-8?q?=E9=87=8D=E6=96=B0=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/versions/WorldListItem.java | 13 ++++++++++++- .../src/main/resources/assets/lang/I18N.properties | 1 + .../main/resources/assets/lang/I18N_zh.properties | 1 + .../resources/assets/lang/I18N_zh_CN.properties | 1 + .../main/java/org/jackhuang/hmcl/game/World.java | 14 +++++++++++--- 5 files changed, 26 insertions(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java index efcf04596d..f068f387c0 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java @@ -24,8 +24,12 @@ import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; +import java.io.IOException; import java.nio.file.Path; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + public final class WorldListItem extends Control { private final World world; private final Path backupsDir; @@ -67,7 +71,14 @@ public void reveal() { } public void showManagePage() { - Controllers.navigate(new WorldManagePage(world, backupsDir, profile, id)); + try { + world.reloadLevelDat(); + Controllers.navigate(new WorldManagePage(world, backupsDir, profile, id)); + } catch (IOException e) { + LOG.warning("Failed to load level dat of world " + world.getFile(), e); + Controllers.showToast(i18n("world.load.fail")); + parent.refresh(); + } } public void launch() { diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 2b40623b81..ebb2083cee 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1191,6 +1191,7 @@ world.info.player.xp_level=Experience Level world.info.random_seed=Seed world.info.time=Game Time world.info.time.format=%s days +world.load.fail=Failed to load world world.locked=In use world.locked.failed=The world is currently in use. Please close the game and try again. world.manage=Worlds diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index e271331a02..775b942f6d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -987,6 +987,7 @@ world.info.player.xp_level=經驗等級 world.info.random_seed=種子碼 world.info.time=遊戲內時間 world.info.time.format=%s 天 +world.load.fail=世界載入失敗 world.locked=使用中 world.locked.failed=該世界正在使用中,請關閉遊戲後重試。 world.game_version=遊戲版本 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 10e55589c1..2a694473bb 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -998,6 +998,7 @@ world.info.player.xp_level=经验等级 world.info.random_seed=种子 world.info.time=游戏内时间 world.info.time.format=%s 天 +world.load.fail=世界加载失败 world.locked=使用中 world.locked.failed=该世界正在使用中,请关闭游戏后重试。 world.manage=世界管理 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java index 26d07689ed..24943caf07 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java @@ -48,6 +48,7 @@ public final class World { private CompoundTag levelData; private Image icon; private boolean isLocked; + private Path levelDataPath; public World(Path file) throws IOException { this.file = file; @@ -152,7 +153,8 @@ private void loadFromDirectory() throws IOException { if (!Files.exists(levelDat)) { // version 20w14infinite levelDat = file.resolve("special_level.dat"); } - loadWorldInfo(levelDat); + loadAndCheckLevelDat(levelDat); + this.levelDataPath = levelDat; isLocked = isLocked(getSessionLockFile()); Path iconFile = file.resolve("icon.png"); @@ -176,7 +178,7 @@ private void loadFromZipImpl(Path root) throws IOException { throw new IOException("Not a valid world zip file since level.dat or special_level.dat cannot be found."); } - loadWorldInfo(levelDat); + loadAndCheckLevelDat(levelDat); Path iconFile = root.resolve("icon.png"); if (Files.isRegularFile(iconFile)) { @@ -209,7 +211,7 @@ private void loadFromZip() throws IOException { } } - private void loadWorldInfo(Path levelDat) throws IOException { + private void loadAndCheckLevelDat(Path levelDat) throws IOException { this.levelData = parseLevelDat(levelDat); CompoundTag data = levelData.get("Data"); if (data == null) @@ -222,6 +224,12 @@ private void loadWorldInfo(Path levelDat) throws IOException { throw new IOException("level.dat missing LastPlayed"); } + public void reloadLevelDat() throws IOException { + if (levelDataPath != null) { + loadAndCheckLevelDat(this.levelDataPath); + } + } + public void rename(String newName) throws IOException { if (!Files.isDirectory(file)) throw new IOException("Not a valid world directory"); From d85eb6312f7b0a8070376c37708745702d005248 Mon Sep 17 00:00:00 2001 From: mine_ Date: Sat, 3 Jan 2026 21:15:19 +0800 Subject: [PATCH 56/66] =?UTF-8?q?feat:=20=E4=B8=96=E7=95=8C=E5=9B=BE?= =?UTF-8?q?=E6=A0=87=E6=B7=BB=E5=8A=A0=E5=88=A0=E9=99=A4=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/WorldInfoPage.java | 30 ++++++++++++++----- .../resources/assets/lang/I18N.properties | 1 + .../resources/assets/lang/I18N_zh.properties | 1 + .../assets/lang/I18N_zh_CN.properties | 1 + 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index af7dc2fffd..ad43af9dfb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.ui.versions; import com.github.steveice10.opennbt.tag.builtin.*; +import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXComboBox; import com.jfoenix.controls.JFXTextField; import javafx.beans.property.SimpleBooleanProperty; @@ -158,21 +159,24 @@ private void updateControls() { iconImageView.setImage(world.getIcon() == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : world.getIcon()); } - StackPane editIconButton = new StackPane(); + JFXButton editIconButton = new JFXButton(); + JFXButton resetIconButton = new JFXButton(); { - editIconButton.setCursor(Cursor.HAND); - editIconButton.setAlignment(Pos.CENTER_LEFT); - FXUtils.setLimitWidth(editIconButton, 12); - FXUtils.setLimitHeight(editIconButton, 12); - editIconButton.getChildren().setAll(SVG.EDIT.createIcon(12)); + //editIconButton.setAlignment(Pos.CENTER_LEFT); + editIconButton.setGraphic(SVG.EDIT.createIcon(20)); editIconButton.setDisable(worldManagePage.isReadOnly()); FXUtils.onClicked(editIconButton, onClickAction); FXUtils.installFastTooltip(editIconButton, i18n("world.icon.change")); + + resetIconButton.setGraphic(SVG.RESTORE.createIcon(20)); + resetIconButton.setDisable(worldManagePage.isReadOnly()); + FXUtils.onClicked(resetIconButton, this::clearWorldIcon); + FXUtils.installFastTooltip(resetIconButton, i18n("world.icon.reset")); } HBox hBox = new HBox(8); hBox.setAlignment(Pos.CENTER_LEFT); - hBox.getChildren().addAll(editIconButton, iconImageView); + hBox.getChildren().addAll(iconImageView, editIconButton, resetIconButton); iconPane.setRight(hBox); } @@ -683,8 +687,18 @@ private void saveImage(Image image, Path path) { iconImageView.setImage(image); Controllers.showToast(i18n("world.icon.change.succeed.toast")); } catch (IOException e) { - LOG.warning(e.getMessage()); + LOG.warning("Fail to save world icon " + e.getMessage()); iconImageView.setImage(oldImage); } } + + private void clearWorldIcon() { + Path output = world.getFile().resolve("icon.png"); + try { + Files.deleteIfExists(output); + iconImageView.setImage(FXUtils.newBuiltinImage("/assets/img/unknown_server.png")); + } catch (IOException e) { + LOG.warning("Fail to delete world icon " + e.getMessage()); + } + } } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index bcf7f1359a..0034146401 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1158,6 +1158,7 @@ world.icon.change.fail.not_64x64.text=The image resolution is %d×%d instead of world.icon.change.succeed.toast=Successfully updated the world icon. world.icon.change.tip=A 64×64 PNG image is required. Images with an incorrect resolution cannot be parsed by Minecraft. world.icon.choose.title=Select world icon +world.icon.reset=Reset World Icon world.import.already_exists=This world already exists. world.import.choose=Choose world archive you want to import world.import.failed=Failed to import this world: %s diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 1b6a4e8a3e..2bfa0ef6af 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -953,6 +953,7 @@ world.icon.change.fail.not_64x64.text=該圖片的解析度為 %d×%d,而不 world.icon.change.succeed.toast=世界圖示修改成功 world.icon.change.tip=請提供一張 64×64 PNG 格式的圖片。錯誤解析度的圖片將無法被Minecraft解析。 world.icon.choose.title=選擇世界圖示 +world.icon.reset=重置世界圖示 world.import.already_exists=此世界已經存在 world.import.choose=選取要匯入的世界壓縮檔 world.import.failed=無法匯入此世界: %s diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index d9d6d72797..936a0c9323 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -964,6 +964,7 @@ world.icon.change.fail.not_64x64.text=该图片的分辨率为 %d×%d,而不 world.icon.change.succeed.toast=世界图标修改成功 world.icon.change.tip=请提供一张 64×64 PNG 格式的图片。错误分辨率的图片将无法被Minecraft解析。 world.icon.choose.title=选择世界图标 +world.icon.reset=重置世界图标 world.import.already_exists=此世界已经存在 world.import.choose=选择要导入的世界压缩包 world.import.failed=无法导入此世界:%s From 8dbd78e3a0bce894bb264ab4c9f9f3acb5855518 Mon Sep 17 00:00:00 2001 From: mine_ Date: Sat, 3 Jan 2026 21:21:06 +0800 Subject: [PATCH 57/66] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9i18n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java | 5 ++--- HMCL/src/main/resources/assets/lang/I18N.properties | 1 - HMCL/src/main/resources/assets/lang/I18N_zh.properties | 1 - HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties | 1 - 4 files changed, 2 insertions(+), 6 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index ad43af9dfb..809152873a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -162,16 +162,15 @@ private void updateControls() { JFXButton editIconButton = new JFXButton(); JFXButton resetIconButton = new JFXButton(); { - //editIconButton.setAlignment(Pos.CENTER_LEFT); editIconButton.setGraphic(SVG.EDIT.createIcon(20)); editIconButton.setDisable(worldManagePage.isReadOnly()); FXUtils.onClicked(editIconButton, onClickAction); - FXUtils.installFastTooltip(editIconButton, i18n("world.icon.change")); + FXUtils.installFastTooltip(editIconButton, i18n("button.edit")); resetIconButton.setGraphic(SVG.RESTORE.createIcon(20)); resetIconButton.setDisable(worldManagePage.isReadOnly()); FXUtils.onClicked(resetIconButton, this::clearWorldIcon); - FXUtils.installFastTooltip(resetIconButton, i18n("world.icon.reset")); + FXUtils.installFastTooltip(resetIconButton, i18n("button.reset")); } HBox hBox = new HBox(8); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index f6f5bace2a..3b898e5005 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1159,7 +1159,6 @@ world.icon.change.fail.not_64x64.text=The image resolution is %d×%d instead of world.icon.change.succeed.toast=Successfully updated the world icon. world.icon.change.tip=A 64×64 PNG image is required. Images with an incorrect resolution cannot be parsed by Minecraft. world.icon.choose.title=Select world icon -world.icon.reset=Reset World Icon world.import.already_exists=This world already exists. world.import.choose=Choose world archive you want to import world.import.failed=Failed to import this world: %s diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 09c77712c8..1d9959c2d9 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -946,7 +946,6 @@ world.icon.change.fail.not_64x64.text=該圖片的解析度為 %d×%d,而不 world.icon.change.succeed.toast=世界圖示修改成功 world.icon.change.tip=請提供一張 64×64 PNG 格式的圖片。錯誤解析度的圖片將無法被Minecraft解析。 world.icon.choose.title=選擇世界圖示 -world.icon.reset=重置世界圖示 world.import.already_exists=此世界已經存在 world.import.choose=選取要匯入的世界壓縮檔 world.import.failed=無法匯入此世界: %s diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 05944eb9ce..2608c6a697 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -951,7 +951,6 @@ world.icon.change.fail.not_64x64.text=该图片的分辨率为 %d×%d,而不 world.icon.change.succeed.toast=世界图标修改成功 world.icon.change.tip=请提供一张 64×64 PNG 格式的图片。错误分辨率的图片将无法被Minecraft解析。 world.icon.choose.title=选择世界图标 -world.icon.reset=重置世界图标 world.import.already_exists=此世界已经存在 world.import.choose=选择要导入的世界压缩包 world.import.failed=无法导入此世界:%s From 23f879bc5356d92c92efa8b8e567b10d64cb3bed Mon Sep 17 00:00:00 2001 From: mine_ Date: Sat, 3 Jan 2026 21:28:56 +0800 Subject: [PATCH 58/66] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index 809152873a..b4d06d6b53 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -166,11 +166,13 @@ private void updateControls() { editIconButton.setDisable(worldManagePage.isReadOnly()); FXUtils.onClicked(editIconButton, onClickAction); FXUtils.installFastTooltip(editIconButton, i18n("button.edit")); + editIconButton.getStyleClass().add("toggle-icon4"); resetIconButton.setGraphic(SVG.RESTORE.createIcon(20)); resetIconButton.setDisable(worldManagePage.isReadOnly()); FXUtils.onClicked(resetIconButton, this::clearWorldIcon); FXUtils.installFastTooltip(resetIconButton, i18n("button.reset")); + resetIconButton.getStyleClass().add("toggle-icon4"); } HBox hBox = new HBox(8); From 0f9e1bd232056cce8690c8328fa70131ae0c31f1 Mon Sep 17 00:00:00 2001 From: mine_ Date: Mon, 5 Jan 2026 21:24:07 +0800 Subject: [PATCH 59/66] =?UTF-8?q?feat:=20=E5=85=88=E5=8A=A0=E9=94=81?= =?UTF-8?q?=E5=90=8E=E6=9B=B4=E6=96=B0=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/versions/WorldListItem.java | 13 +------------ .../jackhuang/hmcl/ui/versions/WorldManagePage.java | 12 +++++++++--- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java index f068f387c0..efcf04596d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java @@ -24,12 +24,8 @@ import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; -import java.io.IOException; import java.nio.file.Path; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; - public final class WorldListItem extends Control { private final World world; private final Path backupsDir; @@ -71,14 +67,7 @@ public void reveal() { } public void showManagePage() { - try { - world.reloadLevelDat(); - Controllers.navigate(new WorldManagePage(world, backupsDir, profile, id)); - } catch (IOException e) { - LOG.warning("Failed to load level dat of world " + world.getFile(), e); - Controllers.showToast(i18n("world.load.fail")); - parent.refresh(); - } + Controllers.navigate(new WorldManagePage(world, backupsDir, profile, id)); } public void launch() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java index fbe1c7ac54..15f06e1643 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java @@ -41,6 +41,7 @@ import java.nio.file.Path; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; /** * @author Glavo @@ -68,6 +69,14 @@ public WorldManagePage(World world, Path backupsDir, Profile profile, String id) this.profile = profile; this.id = id; + sessionLockChannel = WorldManageUIUtils.getSessionLockChannel(world); + try { + world.reloadLevelDat(); + System.out.println("reload level dat"); + } catch (IOException e) { + LOG.warning("Can not load world level.dat of world:" + world.getFile() + e.getMessage()); + } + this.worldInfoTab.setNodeSupplier(() -> new WorldInfoPage(this)); this.worldBackupsTab.setNodeSupplier(() -> new WorldBackupsPage(this)); this.datapackTab.setNodeSupplier(() -> new DatapackListPage(this)); @@ -76,9 +85,6 @@ public WorldManagePage(World world, Path backupsDir, Profile profile, String id) this.header = new TabHeader(transitionPane, worldInfoTab, worldBackupsTab); header.select(worldInfoTab); - // Does it need to be done in the background? - sessionLockChannel = WorldManageUIUtils.getSessionLockChannel(world); - setCenter(transitionPane); BorderPane left = new BorderPane(); From 6d97a71802a60dc8ce8ef2ff4134b2d9278556fe Mon Sep 17 00:00:00 2001 From: mine_ Date: Mon, 5 Jan 2026 21:24:58 +0800 Subject: [PATCH 60/66] feat: update --- .../java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java | 1 - 1 file changed, 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java index 15f06e1643..44b3ea4e93 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java @@ -72,7 +72,6 @@ public WorldManagePage(World world, Path backupsDir, Profile profile, String id) sessionLockChannel = WorldManageUIUtils.getSessionLockChannel(world); try { world.reloadLevelDat(); - System.out.println("reload level dat"); } catch (IOException e) { LOG.warning("Can not load world level.dat of world:" + world.getFile() + e.getMessage()); } From c196ed5540eff082588830e43def6630867baf8a Mon Sep 17 00:00:00 2001 From: mine_ Date: Tue, 6 Jan 2026 22:21:04 +0800 Subject: [PATCH 61/66] feat: update --- .../java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java index 44b3ea4e93..b1964d55a7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java @@ -73,7 +73,7 @@ public WorldManagePage(World world, Path backupsDir, Profile profile, String id) try { world.reloadLevelDat(); } catch (IOException e) { - LOG.warning("Can not load world level.dat of world:" + world.getFile() + e.getMessage()); + LOG.warning("Can not load world level.dat of world: " + world.getFile(), e); } this.worldInfoTab.setNodeSupplier(() -> new WorldInfoPage(this)); From 5259759b5b758eb5e47eb644d580e154218ef16c Mon Sep 17 00:00:00 2001 From: mine_ Date: Tue, 6 Jan 2026 22:30:38 +0800 Subject: [PATCH 62/66] feat: update --- .../org/jackhuang/hmcl/ui/versions/WorldInfoPage.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index b4d06d6b53..0175a83011 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -329,7 +329,7 @@ private void updateControls() { setLeftLabel(lastDeathLocationPane, "world.info.player.last_death_location"); Label lastDeathLocationLabel = new Label(); setRightTextLabel(lastDeathLocationPane, lastDeathLocationLabel, () -> { - Tag tag = player.get("LastDeathLocation");//Valid after 22w14a; prior to this version, the game did not record the last death location data. + Tag tag = player.get("LastDeathLocation");// Valid after 22w14a; prior to this version, the game did not record the last death location data. if (tag instanceof CompoundTag compoundTag) { Dimension dim = Dimension.of(compoundTag.get("dimension")); if (dim != null) { @@ -350,7 +350,7 @@ private void updateControls() { setRightTextLabel(spawnPane, spawnLabel, () -> { Dimension dimension; - if (player.get("respawn") instanceof CompoundTag respawnTag && respawnTag.get("dimension") != null) { //Valid after 25w07a + if (player.get("respawn") instanceof CompoundTag respawnTag && respawnTag.get("dimension") != null) { // Valid after 25w07a dimension = Dimension.of(respawnTag.get("dimension")); Tag posTag = respawnTag.get("pos"); @@ -359,8 +359,8 @@ private void updateControls() { } } else if (player.get("SpawnX") instanceof IntTag intX && player.get("SpawnY") instanceof IntTag intY - && player.get("SpawnZ") instanceof IntTag intZ) { //Valid before 25w07a - //SpawnDimension tag is valid after 20w12a. Prior to this version, the game did not record the respawn point dimension and respawned in the Overworld. + && player.get("SpawnZ") instanceof IntTag intZ) { // Valid before 25w07a + // SpawnDimension tag is valid after 20w12a. Prior to this version, the game did not record the respawn point dimension and respawned in the Overworld. dimension = Dimension.of(player.get("SpawnDimension") == null ? new IntTag("SpawnDimension", 0) : player.get("SpawnDimension")); if (dimension == null) { return ""; From d6a4582da139d6bad16f114324e5cc9521791e02 Mon Sep 17 00:00:00 2001 From: mine_ Date: Tue, 6 Jan 2026 23:05:15 +0800 Subject: [PATCH 63/66] feat: update --- .../java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index 0175a83011..30c06ee7fa 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -688,7 +688,7 @@ private void saveImage(Image image, Path path) { iconImageView.setImage(image); Controllers.showToast(i18n("world.icon.change.succeed.toast")); } catch (IOException e) { - LOG.warning("Fail to save world icon " + e.getMessage()); + LOG.warning("Failed to save world icon " + e.getMessage()); iconImageView.setImage(oldImage); } } @@ -699,7 +699,7 @@ private void clearWorldIcon() { Files.deleteIfExists(output); iconImageView.setImage(FXUtils.newBuiltinImage("/assets/img/unknown_server.png")); } catch (IOException e) { - LOG.warning("Fail to delete world icon " + e.getMessage()); + LOG.warning("Failed to delete world icon " + e.getMessage()); } } } From 3e2114e5f48fb061edfc1e9d2ade7f801186538c Mon Sep 17 00:00:00 2001 From: mine_ Date: Tue, 6 Jan 2026 23:53:11 +0800 Subject: [PATCH 64/66] feat: update --- .../jackhuang/hmcl/ui/versions/WorldManagePage.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java index b1964d55a7..3fa1c926c0 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.ui.versions; import com.jfoenix.controls.JFXPopup; +import javafx.application.Platform; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.SimpleObjectProperty; @@ -27,6 +28,7 @@ import javafx.scene.layout.VBox; import org.jackhuang.hmcl.game.World; import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.animation.TransitionPane; @@ -54,6 +56,8 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco private final Profile profile; private final String id; + private boolean loadFailed = false; + private final TabHeader header; private final TabHeader.Tab worldInfoTab = new TabHeader.Tab<>("worldInfoPage"); private final TabHeader.Tab worldBackupsTab = new TabHeader.Tab<>("worldBackupsPage"); @@ -74,6 +78,7 @@ public WorldManagePage(World world, Path backupsDir, Profile profile, String id) world.reloadLevelDat(); } catch (IOException e) { LOG.warning("Can not load world level.dat of world: " + world.getFile(), e); + loadFailed = true; } this.worldInfoTab.setNodeSupplier(() -> new WorldInfoPage(this)); @@ -164,6 +169,12 @@ public WorldManagePage(World world, Path backupsDir, Profile profile, String id) } private void onNavigated(Navigator.NavigationEvent event) { + if (loadFailed) { + Platform.runLater(() -> { + fireEvent(new PageCloseEvent()); + Controllers.dialog(i18n("world.load.fail"), null, MessageDialogPane.MessageType.ERROR); + }); + } if (sessionLockChannel == null || !sessionLockChannel.isOpen()) { sessionLockChannel = WorldManageUIUtils.getSessionLockChannel(world); } From 834960acb69aae7615ecc6132022282384709153 Mon Sep 17 00:00:00 2001 From: mine_ Date: Wed, 7 Jan 2026 00:02:43 +0800 Subject: [PATCH 65/66] feat: update --- .../java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java | 1 + 1 file changed, 1 insertion(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java index 3fa1c926c0..848beaeed3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java @@ -174,6 +174,7 @@ private void onNavigated(Navigator.NavigationEvent event) { fireEvent(new PageCloseEvent()); Controllers.dialog(i18n("world.load.fail"), null, MessageDialogPane.MessageType.ERROR); }); + return; } if (sessionLockChannel == null || !sessionLockChannel.isOpen()) { sessionLockChannel = WorldManageUIUtils.getSessionLockChannel(world); From 82a5f36d9468e8de448cedb2f6e89c5dc27dd7be Mon Sep 17 00:00:00 2001 From: mine_ Date: Wed, 7 Jan 2026 08:45:51 +0800 Subject: [PATCH 66/66] =?UTF-8?q?feat:=20=E5=BA=94=E7=94=A83gf8jv4dv?= =?UTF-8?q?=E7=9A=84=E5=BB=BA=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HMCL/src/main/resources/assets/lang/I18N.properties | 2 +- HMCL/src/main/resources/assets/lang/I18N_zh.properties | 2 +- HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 3b898e5005..7cbadeed02 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1155,7 +1155,7 @@ world.icon.change=Change world icon world.icon.change.fail.load.title=Failed to parse image world.icon.change.fail.load.text=This image appears to be corrupted, and HMCL cannot parse it. world.icon.change.fail.not_64x64.title=Image size error -world.icon.change.fail.not_64x64.text=The image resolution is %d×%d instead of 64x64. Please provide a 64×64 image and try again. +world.icon.change.fail.not_64x64.text=The image resolution is %d×%d instead of 64×64. Please provide a 64×64 image and try again. world.icon.change.succeed.toast=Successfully updated the world icon. world.icon.change.tip=A 64×64 PNG image is required. Images with an incorrect resolution cannot be parsed by Minecraft. world.icon.choose.title=Select world icon diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 1d9959c2d9..190204469c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -944,7 +944,7 @@ world.icon.change.fail.load.text=該圖片似乎已損毀,HMCL 無法解析它 world.icon.change.fail.not_64x64.title=圖片大小錯誤 world.icon.change.fail.not_64x64.text=該圖片的解析度為 %d×%d,而不是 64×64,請提供一張 64×64 解析度的圖片再次嘗試 world.icon.change.succeed.toast=世界圖示修改成功 -world.icon.change.tip=請提供一張 64×64 PNG 格式的圖片。錯誤解析度的圖片將無法被Minecraft解析。 +world.icon.change.tip=請提供一張 64×64 PNG 格式的圖片。錯誤解析度的圖片將無法被 Minecraft 解析。 world.icon.choose.title=選擇世界圖示 world.import.already_exists=此世界已經存在 world.import.choose=選取要匯入的世界壓縮檔 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 2608c6a697..4bc19a5e19 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -949,7 +949,7 @@ world.icon.change.fail.load.text=该图片似乎已损坏,HMCL 无法解析它 world.icon.change.fail.not_64x64.title=图片大小错误 world.icon.change.fail.not_64x64.text=该图片的分辨率为 %d×%d,而不是 64×64,请提供一张 64×64 分辨率的图片再次尝试 world.icon.change.succeed.toast=世界图标修改成功 -world.icon.change.tip=请提供一张 64×64 PNG 格式的图片。错误分辨率的图片将无法被Minecraft解析。 +world.icon.change.tip=请提供一张 64×64 PNG 格式的图片。错误分辨率的图片将无法被 Minecraft 解析。 world.icon.choose.title=选择世界图标 world.import.already_exists=此世界已经存在 world.import.choose=选择要导入的世界压缩包