From a02e44b41db611857460047700c598c3d4330b72 Mon Sep 17 00:00:00 2001 From: mine_ Date: Tue, 18 Nov 2025 22:21:05 +0800 Subject: [PATCH 01/30] =?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/30] 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/30] 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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] 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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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/30] =?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;