diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPage.java index 49198822c2..deff795f8e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPage.java @@ -17,6 +17,7 @@ */ package org.jackhuang.hmcl.ui.versions; +import javafx.beans.property.BooleanProperty; import javafx.collections.ObservableList; import javafx.scene.control.Skin; import javafx.stage.FileChooser; @@ -42,20 +43,27 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public final class DatapackListPage extends ListPageBase { +public final class DatapackListPage extends ListPageBase implements WorldManagePage.WorldRefreshable { private final Path worldDir; private final Datapack datapack; + final BooleanProperty isReadOnlyProperty; public DatapackListPage(WorldManagePage worldManagePage) { this.worldDir = worldManagePage.getWorld().getFile(); datapack = new Datapack(worldDir.resolve("datapacks")); setItems(MappedObservableList.create(datapack.getPacks(), DatapackListPageSkin.DatapackInfoObject::new)); + isReadOnlyProperty = worldManagePage.readOnlyProperty(); FXUtils.applyDragListener(this, it -> Objects.equals("zip", FileUtils.getExtension(it)), - mods -> mods.forEach(this::installSingleDatapack), this::refresh); + this::installMutiDatapack, this::refresh); refresh(); } + private void installMutiDatapack(List datapackPath) { + datapackPath.forEach(this::installSingleDatapack); + Controllers.showToast(i18n("datapack.reload.toast")); + } + private void installSingleDatapack(Path datapack) { try { this.datapack.installPack(datapack); @@ -83,7 +91,7 @@ public void add() { List res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage())); if (res != null) { - res.forEach(this::installSingleDatapack); + installMutiDatapack(res); } datapack.loadFromDir(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPageSkin.java index a6698bc2e6..40afeb7a31 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPageSkin.java @@ -66,6 +66,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.stream.IntStream; +import java.util.stream.Stream; import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -114,16 +115,21 @@ final class DatapackListPageSkin extends SkinBase { createToolbarButton2(i18n("search"), SVG.SEARCH, () -> isSearching.set(true)) ); + JFXButton removeButton = createToolbarButton2(i18n("button.remove"), SVG.DELETE, () -> { + Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> { + skinnable.removeSelected(listView.getSelectionModel().getSelectedItems()); + }, null); + }); + JFXButton enableButton = createToolbarButton2(i18n("mods.enable"), SVG.CHECK, () -> + skinnable.enableSelected(listView.getSelectionModel().getSelectedItems())); + JFXButton disableButton = createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () -> + skinnable.disableSelected(listView.getSelectionModel().getSelectedItems())); + Stream.of(removeButton, enableButton, disableButton).forEach((button) -> button.disableProperty().bind(getSkinnable().isReadOnlyProperty)); + selectingToolbar.getChildren().addAll( - createToolbarButton2(i18n("button.remove"), SVG.DELETE, () -> { - Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> { - skinnable.removeSelected(listView.getSelectionModel().getSelectedItems()); - }, null); - }), - createToolbarButton2(i18n("mods.enable"), SVG.CHECK, () -> - skinnable.enableSelected(listView.getSelectionModel().getSelectedItems())), - createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () -> - skinnable.disableSelected(listView.getSelectionModel().getSelectedItems())), + removeButton, + enableButton, + disableButton, createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> listView.getSelectionModel().selectRange(0, listView.getItems().size())),//reason for not using selectAll() is that selectAll() first clears all selected then selects all, causing the toolbar to flicker createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () -> @@ -179,7 +185,7 @@ final class DatapackListPageSkin extends SkinBase { center.getStyleClass().add("large-spinner-pane"); center.loadingProperty().bind(skinnable.loadingProperty()); - listView.setCellFactory(x -> new DatapackInfoListCell(listView)); + listView.setCellFactory(x -> new DatapackInfoListCell(listView, getSkinnable().isReadOnlyProperty)); listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); this.listView.setItems(filteredList); @@ -302,7 +308,7 @@ private final class DatapackInfoListCell extends MDListCell final TwoLineListItem content = new TwoLineListItem(); BooleanProperty booleanProperty; - DatapackInfoListCell(JFXListView listView) { + DatapackInfoListCell(JFXListView listView, BooleanProperty isReadOnlyProperty) { super(listView); HBox container = new HBox(8); @@ -312,6 +318,8 @@ private final class DatapackInfoListCell extends MDListCell content.setMouseTransparent(true); setSelectable(); + checkBox.disableProperty().bind(isReadOnlyProperty); + imageView.setFitWidth(32); imageView.setFitHeight(32); imageView.setPreserveRatio(true); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupsPage.java index 02901d0687..0f85789dc7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupsPage.java @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.ui.versions; import com.jfoenix.controls.JFXButton; +import javafx.beans.property.BooleanProperty; import javafx.collections.FXCollections; import javafx.geometry.Insets; import javafx.geometry.Pos; @@ -61,18 +62,18 @@ /** * @author Glavo */ -public final class WorldBackupsPage extends ListPageBase { +public final class WorldBackupsPage extends ListPageBase implements WorldManagePage.WorldRefreshable { static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss"); private final World world; private final Path backupsDir; - private final boolean isReadOnly; + private final BooleanProperty readOnlyProperty; private final Pattern backupFileNamePattern; public WorldBackupsPage(WorldManagePage worldManagePage) { this.world = worldManagePage.getWorld(); this.backupsDir = worldManagePage.getBackupsDir(); - this.isReadOnly = worldManagePage.isReadOnly(); + this.readOnlyProperty = worldManagePage.readOnlyProperty(); this.backupFileNamePattern = Pattern.compile("(?[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2})_" + Pattern.quote(world.getFileName()) + "( (?[0-9]+))?\\.zip"); refresh(); @@ -164,7 +165,7 @@ private final class WorldBackupsPageSkin extends ToolbarListPageSkin initializeToolbar(WorldBackupsPage skinnable) { JFXButton createBackup = createToolbarButton2(i18n("world.backup.create.new_one"), SVG.ARCHIVE, skinnable::createBackup); - createBackup.setDisable(isReadOnly); + createBackup.disableProperty().bind(getSkinnable().readOnlyProperty); return Arrays.asList( createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), 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 30c06ee7fa..c1dea93f5c 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 @@ -36,7 +36,6 @@ 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.task.Schedulers; import org.jackhuang.hmcl.task.Task; @@ -44,13 +43,16 @@ import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.*; +import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.io.FileUtils; import org.jetbrains.annotations.PropertyKey; -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.Duration; import java.time.Instant; import java.util.Arrays; import java.util.Locale; @@ -63,8 +65,9 @@ /** * @author Glavo */ -public final class WorldInfoPage extends SpinnerPane { +public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.WorldRefreshable { private final WorldManagePage worldManagePage; + private boolean isReadOnly; private final World world; private CompoundTag levelDat; @@ -73,19 +76,7 @@ public final class WorldInfoPage extends SpinnerPane { public WorldInfoPage(WorldManagePage worldManagePage) { this.worldManagePage = worldManagePage; this.world = worldManagePage.getWorld(); - - this.setLoading(true); - Task.supplyAsync(this::loadWorldInfo) - .whenComplete(Schedulers.javafx(), ((result, exception) -> { - if (exception == null) { - this.levelDat = result; - updateControls(); - setLoading(false); - } else { - LOG.warning("Failed to load level.dat", exception); - setFailedReason(i18n("world.info.failed")); - } - })).start(); + refresh(); } private CompoundTag loadWorldInfo() throws IOException { @@ -97,7 +88,6 @@ private CompoundTag loadWorldInfo() throws IOException { private void updateControls() { CompoundTag dataTag = levelDat.get("Data"); - CompoundTag worldGenSettings = dataTag.get("WorldGenSettings"); ScrollPane scrollPane = new ScrollPane(); scrollPane.setFitToHeight(true); @@ -111,7 +101,7 @@ private void updateControls() { FXUtils.smoothScrolling(scrollPane); rootPane.getStyleClass().add("card-list"); - ComponentList basicInfo = new ComponentList(); + ComponentList worldInfo = new ComponentList(); { BorderPane worldNamePane = new BorderPane(); { @@ -119,14 +109,13 @@ private void updateControls() { JFXTextField worldNameField = new JFXTextField(); setRightTextField(worldNamePane, worldNameField, 200); - Tag tag = dataTag.get("LevelName"); - if (tag instanceof StringTag stringTag) { - worldNameField.setText(stringTag.getValue()); - - worldNameField.textProperty().addListener((observable, oldValue, newValue) -> { - if (newValue != null) { + if (dataTag.get("LevelName") instanceof StringTag worldNameTag) { + worldNameField.setText(worldNameTag.getValue()); + worldNameField.focusedProperty().addListener((observable, oldValue, newValue) -> { + if (!newValue && StringUtils.isNotBlank(worldNameField.getText())) { try { - world.setWorldName(newValue); + world.setWorldName(worldNameField.getText()); + worldManagePage.changeStateTitle(i18n("world.manage.title", StringUtils.parseColorEscapes(world.getWorldName()))); } catch (Exception e) { LOG.warning("Failed to set world name", e); } @@ -140,8 +129,7 @@ private void updateControls() { BorderPane gameVersionPane = new BorderPane(); { setLeftLabel(gameVersionPane, "world.info.game_version"); - Label gameVersionLabel = new Label(); - setRightTextLabel(gameVersionPane, gameVersionLabel, () -> world.getGameVersion() == null ? "" : world.getGameVersion().toNormalizedString()); + setRightTextLabel(gameVersionPane, () -> world.getGameVersion() == null ? "" : world.getGameVersion().toNormalizedString()); } BorderPane iconPane = new BorderPane(); @@ -149,13 +137,15 @@ private void updateControls() { setLeftLabel(iconPane, "world.icon"); Runnable onClickAction = () -> Controllers.confirm( - i18n("world.icon.change.tip"), i18n("world.icon.change"), MessageDialogPane.MessageType.INFO, + i18n("world.icon.change.tip"), + i18n("world.icon.change"), + MessageDialogPane.MessageType.INFO, this::changeWorldIcon, null ); - FXUtils.limitSize(iconImageView, 32, 32); { + FXUtils.limitSize(iconImageView, 32, 32); iconImageView.setImage(world.getIcon() == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : world.getIcon()); } @@ -163,13 +153,13 @@ private void updateControls() { JFXButton resetIconButton = new JFXButton(); { editIconButton.setGraphic(SVG.EDIT.createIcon(20)); - editIconButton.setDisable(worldManagePage.isReadOnly()); + editIconButton.setDisable(isReadOnly); FXUtils.onClicked(editIconButton, onClickAction); FXUtils.installFastTooltip(editIconButton, i18n("button.edit")); editIconButton.getStyleClass().add("toggle-icon4"); resetIconButton.setGraphic(SVG.RESTORE.createIcon(20)); - resetIconButton.setDisable(worldManagePage.isReadOnly()); + resetIconButton.setDisable(isReadOnly); FXUtils.onClicked(resetIconButton, this::clearWorldIcon); FXUtils.installFastTooltip(resetIconButton, i18n("button.reset")); resetIconButton.getStyleClass().add("toggle-icon4"); @@ -190,9 +180,7 @@ private void updateControls() { StackPane visibilityButton = new StackPane(); { visibilityButton.setCursor(Cursor.HAND); - visibilityButton.setAlignment(Pos.BOTTOM_RIGHT); - FXUtils.setLimitWidth(visibilityButton, 12); - FXUtils.setLimitHeight(visibilityButton, 12); + visibilityButton.setAlignment(Pos.CENTER_RIGHT); FXUtils.onClicked(visibilityButton, () -> visibility.set(!visibility.get())); } @@ -220,23 +208,38 @@ private void updateControls() { } } + BorderPane worldSpawnPoint = new BorderPane(); + { + setLeftLabel(worldSpawnPoint, "world.info.spawn"); + setRightTextLabel(worldSpawnPoint, () -> { + if (dataTag.get("spawn") instanceof CompoundTag spawnTag && spawnTag.get("pos") instanceof IntArrayTag posTag) { + return Dimension.of(spawnTag.get("dimension") instanceof StringTag dimensionTag + ? dimensionTag + : new StringTag("SpawnDimension", "minecraft:overworld")) + .formatPosition(posTag); + } else if (dataTag.get("SpawnX") instanceof IntTag intX + && dataTag.get("SpawnY") instanceof IntTag intY + && dataTag.get("SpawnZ") instanceof IntTag intZ) { + return Dimension.OVERWORLD.formatPosition(intX.getValue(), intY.getValue(), intZ.getValue()); + } else { + return ""; + } + }); + } + BorderPane lastPlayedPane = new BorderPane(); { setLeftLabel(lastPlayedPane, "world.info.last_played"); - Label lastPlayedLabel = new Label(); - setRightTextLabel(lastPlayedPane, lastPlayedLabel, () -> formatDateTime(Instant.ofEpochMilli(world.getLastPlayed()))); + setRightTextLabel(lastPlayedPane, () -> formatDateTime(Instant.ofEpochMilli(world.getLastPlayed()))); } BorderPane timePane = new BorderPane(); { 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; - return i18n("world.info.time.format", days); + setRightTextLabel(timePane, () -> { + if (dataTag.get("Time") instanceof LongTag timeTag) { + Duration duration = Duration.ofSeconds(timeTag.getValue() / 20); + return i18n("world.info.time.format", duration.toDays(), duration.toHoursPart(), duration.toMinutesPart()); } else { return ""; } @@ -246,19 +249,20 @@ private void updateControls() { OptionToggleButton allowCheatsButton = new OptionToggleButton(); { allowCheatsButton.setTitle(i18n("world.info.allow_cheats")); - allowCheatsButton.setDisable(worldManagePage.isReadOnly()); - Tag tag = dataTag.get("allowCommands"); + allowCheatsButton.setDisable(isReadOnly); - checkTagAndSetListener(tag, allowCheatsButton); + bindTagAndToggleButton(dataTag.get("allowCommands"), allowCheatsButton); } OptionToggleButton generateFeaturesButton = new OptionToggleButton(); { generateFeaturesButton.setTitle(i18n("world.info.generate_features")); - generateFeaturesButton.setDisable(worldManagePage.isReadOnly()); - Tag tag = worldGenSettings != null ? worldGenSettings.get("generate_features") : dataTag.get("MapFeatures"); + generateFeaturesButton.setDisable(isReadOnly); - checkTagAndSetListener(tag, generateFeaturesButton); + CompoundTag worldGenSettings = dataTag.get("WorldGenSettings"); + // generate_features was valid after 20w20a and MapFeatures was before that + Tag generateFeaturesTag = worldGenSettings != null ? worldGenSettings.get("generate_features") : dataTag.get("MapFeatures"); + bindTagAndToggleButton(generateFeaturesTag, generateFeaturesButton); } BorderPane difficultyPane = new BorderPane(); @@ -266,18 +270,17 @@ private void updateControls() { setLeftLabel(difficultyPane, "world.info.difficulty"); JFXComboBox difficultyBox = new JFXComboBox<>(Difficulty.items); - difficultyBox.setDisable(worldManagePage.isReadOnly()); + difficultyBox.setDisable(isReadOnly); BorderPane.setAlignment(difficultyBox, Pos.CENTER_RIGHT); difficultyPane.setRight(difficultyBox); - Tag tag = dataTag.get("Difficulty"); - if (tag instanceof ByteTag byteTag) { - Difficulty difficulty = Difficulty.of(byteTag.getValue()); + if (dataTag.get("Difficulty") instanceof ByteTag difficultyTag) { + Difficulty difficulty = Difficulty.of(difficultyTag.getValue()); if (difficulty != null) { difficultyBox.setValue(difficulty); difficultyBox.valueProperty().addListener((o, oldValue, newValue) -> { if (newValue != null) { - byteTag.setValue((byte) newValue.ordinal()); + difficultyTag.setValue((byte) newValue.ordinal()); saveLevelDat(); } }); @@ -292,33 +295,28 @@ private void updateControls() { OptionToggleButton difficultyLockPane = new OptionToggleButton(); { difficultyLockPane.setTitle(i18n("world.info.difficulty_lock")); - difficultyLockPane.setDisable(worldManagePage.isReadOnly()); + difficultyLockPane.setDisable(isReadOnly); - Tag tag = dataTag.get("DifficultyLocked"); - checkTagAndSetListener(tag, difficultyLockPane); + bindTagAndToggleButton(dataTag.get("DifficultyLocked"), difficultyLockPane); } - basicInfo.getContent().setAll( - worldNamePane, gameVersionPane, iconPane, seedPane, lastPlayedPane, timePane, + worldInfo.getContent().setAll( + worldNamePane, gameVersionPane, iconPane, seedPane, worldSpawnPoint, lastPlayedPane, timePane, allowCheatsButton, generateFeaturesButton, difficultyPane, difficultyLockPane); - rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info.basic")), basicInfo); + rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info")), worldInfo); } - Tag playerTag = dataTag.get("Player"); - if (playerTag instanceof CompoundTag player) { + if (dataTag.get("Player") instanceof CompoundTag playerTag) { ComponentList playerInfo = new ComponentList(); BorderPane locationPane = new BorderPane(); { 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) - return posString; + setRightTextLabel(locationPane, () -> { + Dimension dimension = Dimension.of(playerTag.get("Dimension")); + if (dimension != null && playerTag.get("Pos") instanceof ListTag posTag) { + return dimension.formatPosition(posTag); } return ""; }); @@ -327,15 +325,12 @@ private void updateControls() { BorderPane lastDeathLocationPane = new BorderPane(); { setLeftLabel(lastDeathLocationPane, "world.info.player.last_death_location"); - Label lastDeathLocationLabel = new Label(); - setRightTextLabel(lastDeathLocationPane, lastDeathLocationLabel, () -> { - Tag tag = player.get("LastDeathLocation");// Valid after 22w14a; prior to this version, the game did not record the last death location data. - 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; + setRightTextLabel(lastDeathLocationPane, () -> { + // Valid after 22w14a; prior to this version, the game did not record the last death location data. + if (playerTag.get("LastDeathLocation") instanceof CompoundTag LastDeathLocationTag) { + Dimension dimension = Dimension.of(LastDeathLocationTag.get("dimension")); + if (dimension != null && LastDeathLocationTag.get("pos") instanceof IntArrayTag posTag) { + return dimension.formatPosition(posTag); } } return ""; @@ -346,27 +341,19 @@ private void updateControls() { BorderPane spawnPane = new BorderPane(); { setLeftLabel(spawnPane, "world.info.player.spawn"); - Label spawnLabel = new Label(); - setRightTextLabel(spawnPane, spawnLabel, () -> { - - Dimension dimension; - if (player.get("respawn") instanceof CompoundTag respawnTag && respawnTag.get("dimension") != null) { // Valid after 25w07a - dimension = Dimension.of(respawnTag.get("dimension")); - Tag posTag = respawnTag.get("pos"); - - if (posTag instanceof IntArrayTag intArrayTag && intArrayTag.length() >= 3) { - return dimension.formatPosition(intArrayTag.getValue(0), intArrayTag.getValue(1), intArrayTag.getValue(2)); - } - } else if (player.get("SpawnX") instanceof IntTag intX - && player.get("SpawnY") instanceof IntTag intY - && player.get("SpawnZ") instanceof IntTag intZ) { // Valid before 25w07a + setRightTextLabel(spawnPane, () -> { + + if (playerTag.get("respawn") instanceof CompoundTag respawnTag + && respawnTag.get("dimension") instanceof StringTag dimensionTag + && respawnTag.get("pos") instanceof IntArrayTag intArrayTag + && intArrayTag.length() >= 3) { // Valid after 25w07a + return Dimension.of(dimensionTag).formatPosition(intArrayTag); + } else if (playerTag.get("SpawnX") instanceof IntTag intX + && playerTag.get("SpawnY") instanceof IntTag intY + && playerTag.get("SpawnZ") instanceof IntTag intZ) { // Valid before 25w07a // SpawnDimension tag is valid after 20w12a. Prior to this version, the game did not record the respawn point dimension and respawned in the Overworld. - dimension = Dimension.of(player.get("SpawnDimension") == null ? new IntTag("SpawnDimension", 0) : player.get("SpawnDimension")); - if (dimension == null) { - return ""; - } - - return dimension.formatPosition(intX.getValue(), intY.getValue(), intZ.getValue()); + return (playerTag.get("SpawnDimension") instanceof StringTag dimensionTag ? Dimension.of(dimensionTag) : Dimension.OVERWORLD) + .formatPosition(intX.getValue(), intY.getValue(), intZ.getValue()); } return ""; @@ -378,30 +365,26 @@ private void updateControls() { setLeftLabel(playerGameTypePane, "world.info.player.game_type"); JFXComboBox gameTypeBox = new JFXComboBox<>(GameType.items); - gameTypeBox.setDisable(worldManagePage.isReadOnly()); + gameTypeBox.setDisable(isReadOnly); BorderPane.setAlignment(gameTypeBox, Pos.CENTER_RIGHT); playerGameTypePane.setRight(gameTypeBox); - Tag tag = player.get("playerGameType"); - Tag hardcoreTag = dataTag.get("hardcore"); - boolean isHardcore = hardcoreTag instanceof ByteTag && ((ByteTag) hardcoreTag).getValue() == 1; + IntTag playerGameTypeTag = playerTag.get("playerGameType"); + ByteTag hardcoreTag = dataTag.get("hardcore"); - if (tag instanceof IntTag intTag) { - GameType gameType = GameType.of(intTag.getValue(), isHardcore); + if (playerGameTypeTag != null && hardcoreTag != null) { + boolean isHardcore = hardcoreTag.getValue() == 1; + GameType gameType = GameType.of(playerGameTypeTag.getValue(), isHardcore); if (gameType != null) { gameTypeBox.setValue(gameType); gameTypeBox.valueProperty().addListener((o, oldValue, newValue) -> { if (newValue != null) { if (newValue == GameType.HARDCORE) { - intTag.setValue(0); // survival (hardcore worlds are survival+hardcore flag) - if (hardcoreTag instanceof ByteTag) { - ((ByteTag) hardcoreTag).setValue((byte) 1); - } + playerGameTypeTag.setValue(0); // survival (hardcore worlds are survival+hardcore flag) + hardcoreTag.setValue((byte) 1); } else { - intTag.setValue(newValue.ordinal()); - if (hardcoreTag instanceof ByteTag) { - ((ByteTag) hardcoreTag).setValue((byte) 0); - } + playerGameTypeTag.setValue(newValue.ordinal()); + hardcoreTag.setValue((byte) 0); } saveLevelDat(); } @@ -417,48 +400,30 @@ private void updateControls() { BorderPane healthPane = new BorderPane(); { setLeftLabel(healthPane, "world.info.player.health"); - JFXTextField healthField = new JFXTextField(); - setRightTextField(healthPane, healthField, 50); - - Tag tag = player.get("Health"); - if (tag instanceof FloatTag floatTag) { - setTagAndTextField(floatTag, healthField); - } else { - healthField.setDisable(true); - } + setRightTextField(healthPane, 50, playerTag.get("Health")); } BorderPane foodLevelPane = new BorderPane(); { setLeftLabel(foodLevelPane, "world.info.player.food_level"); - JFXTextField foodLevelField = new JFXTextField(); - setRightTextField(foodLevelPane, foodLevelField, 50); + setRightTextField(foodLevelPane, 50, playerTag.get("foodLevel")); + } - Tag tag = player.get("foodLevel"); - if (tag instanceof IntTag intTag) { - setTagAndTextField(intTag, foodLevelField); - } else { - foodLevelField.setDisable(true); - } + BorderPane foodSaturationPane = new BorderPane(); + { + setLeftLabel(foodSaturationPane, "world.info.player.food_saturation_level"); + setRightTextField(foodSaturationPane, 50, playerTag.get("foodSaturationLevel")); } BorderPane xpLevelPane = new BorderPane(); { setLeftLabel(xpLevelPane, "world.info.player.xp_level"); - JFXTextField xpLevelField = new JFXTextField(); - setRightTextField(xpLevelPane, xpLevelField, 50); - - Tag tag = player.get("XpLevel"); - if (tag instanceof IntTag intTag) { - setTagAndTextField(intTag, xpLevelField); - } else { - xpLevelField.setDisable(true); - } + setRightTextField(xpLevelPane, 50, playerTag.get("XpLevel")); } playerInfo.getContent().setAll( - locationPane, lastDeathLocationPane, spawnPane, - playerGameTypePane, healthPane, foodLevelPane, xpLevelPane + locationPane, lastDeathLocationPane, spawnPane, playerGameTypePane, + healthPane, foodLevelPane, foodSaturationPane, xpLevelPane ); rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info.player")), playerInfo); @@ -471,14 +436,27 @@ private void setLeftLabel(BorderPane borderPane, @PropertyKey(resourceBundle = " borderPane.setLeft(label); } + private void setRightTextField(BorderPane borderPane, int perfWidth, Tag tag) { + JFXTextField textField = new JFXTextField(); + setRightTextField(borderPane, textField, perfWidth); + if (tag instanceof IntTag intTag) { + bindTagAndTextField(intTag, textField); + } else if (tag instanceof FloatTag floatTag) { + bindTagAndTextField(floatTag, textField); + } else { + textField.setDisable(true); + } + } + private void setRightTextField(BorderPane borderPane, JFXTextField textField, int perfWidth) { - textField.setDisable(worldManagePage.isReadOnly()); + textField.setDisable(isReadOnly); textField.setPrefWidth(perfWidth); - textField.setAlignment(Pos.CENTER_RIGHT); + BorderPane.setAlignment(textField, Pos.CENTER_RIGHT); borderPane.setRight(textField); } - private void setRightTextLabel(BorderPane borderPane, Label label, Callable setNameCall) { + private void setRightTextLabel(BorderPane borderPane, Callable setNameCall) { + Label label = new Label(); FXUtils.copyOnDoubleClick(label); BorderPane.setAlignment(label, Pos.CENTER_RIGHT); try { @@ -489,7 +467,7 @@ private void setRightTextLabel(BorderPane borderPane, Label label, Callable { if (newValue != null) { try { - intTag.setValue(Integer.parseInt(newValue)); - saveLevelDat(); + Integer integer = Lang.toIntOrNull(newValue); + if (integer != null) { + intTag.setValue(integer); + saveLevelDat(); + } } catch (Exception e) { jfxTextField.setText(oldValue); LOG.warning("Exception happened when saving level.dat", e); @@ -529,14 +510,17 @@ private void setTagAndTextField(IntTag intTag, JFXTextField jfxTextField) { jfxTextField.setValidators(new NumberValidator(i18n("input.number"), true)); } - private void setTagAndTextField(FloatTag floatTag, JFXTextField jfxTextField) { - jfxTextField.setText(new DecimalFormat("#").format(floatTag.getValue().floatValue())); + private void bindTagAndTextField(FloatTag floatTag, JFXTextField jfxTextField) { + jfxTextField.setText(new DecimalFormat("0.#").format(floatTag.getValue())); jfxTextField.textProperty().addListener((o, oldValue, newValue) -> { if (newValue != null) { try { - floatTag.setValue(Float.parseFloat(newValue)); - saveLevelDat(); + Float floatValue = Lang.toFloatOrNull(newValue); + if (floatValue != null) { + floatTag.setValue(floatValue); + saveLevelDat(); + } } catch (Exception e) { jfxTextField.setText(oldValue); LOG.warning("Exception happened when saving level.dat", e); @@ -556,6 +540,22 @@ private void saveLevelDat() { } } + public void refresh() { + this.isReadOnly = worldManagePage.isReadOnly(); + this.setLoading(true); + Task.supplyAsync(this::loadWorldInfo) + .whenComplete(Schedulers.javafx(), ((result, exception) -> { + if (exception == null) { + this.levelDat = result; + updateControls(); + setLoading(false); + } else { + LOG.warning("Failed to load level.dat", exception); + setFailedReason(i18n("world.info.failed")); + } + })).start(); + } + 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")); @@ -565,8 +565,8 @@ static Dimension of(Tag tag) { if (tag instanceof IntTag intTag) { return switch (intTag.getValue()) { case 0 -> OVERWORLD; - case 1 -> THE_NETHER; - case 2 -> THE_END; + case -1 -> THE_NETHER; + case 1 -> THE_END; default -> null; }; } else if (tag instanceof StringTag stringTag) { @@ -662,12 +662,12 @@ private void changeWorldIcon() { fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.png"), "*.png")); fileChooser.setInitialFileName("icon.png"); - File file = fileChooser.showOpenDialog(Controllers.getStage()); - if (file == null) return; + Path iconPath = FileUtils.toPath(fileChooser.showOpenDialog(Controllers.getStage())); + if (iconPath == null) return; Image image; try { - image = FXUtils.loadImage(file.toPath()); + image = FXUtils.loadImage(iconPath); } catch (Exception e) { LOG.warning("Failed to load image", e); Controllers.dialog(i18n("world.icon.change.fail.load.text"), i18n("world.icon.change.fail.load.title"), MessageDialogPane.MessageType.ERROR); @@ -675,16 +675,16 @@ private void changeWorldIcon() { } if ((int) image.getWidth() == 64 && (int) image.getHeight() == 64) { Path output = world.getFile().resolve("icon.png"); - saveImage(image, output); + saveWorldIcon(iconPath, image, output); } else { Controllers.dialog(i18n("world.icon.change.fail.not_64x64.text", (int) image.getWidth(), (int) image.getHeight()), i18n("world.icon.change.fail.not_64x64.title"), MessageDialogPane.MessageType.ERROR); } } - private void saveImage(Image image, Path path) { + private void saveWorldIcon(Path sourcePath, Image image, Path targetPath) { Image oldImage = iconImageView.getImage(); try { - PNGJavaFXUtils.writeImage(image, path); + FileUtils.copyFile(sourcePath, targetPath); iconImageView.setImage(image); Controllers.showToast(i18n("world.icon.change.succeed.toast")); } catch (IOException e) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java index 3d3f74240b..f34650fa24 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java @@ -65,10 +65,9 @@ public final class WorldListPage extends ListPageBase implements VersionP private final BooleanProperty showAll = new SimpleBooleanProperty(this, "showAll", false); private Path savesDir; - private Path backupsDir; private List worlds; private Profile profile; - private String id; + private String versionId; private int refreshCount = 0; @@ -88,9 +87,8 @@ protected Skin createDefaultSkin() { @Override public void loadVersion(Profile profile, String id) { this.profile = profile; - this.id = id; + this.versionId = id; this.savesDir = profile.getRepository().getSavesDirectory(id); - this.backupsDir = profile.getRepository().getBackupsDirectory(id); refresh(); } @@ -100,27 +98,21 @@ private void updateWorldList() { } else if (showAll.get()) { getItems().setAll(worlds); } else { - GameVersionNumber gameVersion = profile.getRepository().getGameVersion(id).map(GameVersionNumber::asGameVersion).orElse(null); - getItems().setAll(worlds.stream() - .filter(world -> world.getGameVersion() == null || world.getGameVersion().equals(gameVersion)) - .toList()); + GameVersionNumber gameVersion = profile.getRepository().getGameVersion(versionId).map(GameVersionNumber::asGameVersion).orElse(null); + getItems().setAll(worlds.stream().filter(world -> world.getGameVersion() == null || world.getGameVersion().equals(gameVersion)).toList()); } } public void refresh() { - if (profile == null || id == null) - return; + if (profile == null || versionId == null) return; int currentRefresh = ++refreshCount; setLoading(true); - Task.supplyAsync(Schedulers.io(), () -> { + Task.runAsync(() -> { // Ensure the game version number is parsed - profile.getRepository().getGameVersion(id); - try (Stream stream = World.getWorlds(savesDir)) { - return stream.toList(); - } - }).whenComplete(Schedulers.javafx(), (result, exception) -> { + profile.getRepository().getGameVersion(versionId); + }).thenSupplyAsync(() -> World.getWorlds(savesDir)).whenComplete(Schedulers.javafx(), (result, exception) -> { if (refreshCount != currentRefresh) { // A newer refresh task is running, discard this result return; @@ -129,8 +121,7 @@ public void refresh() { worlds = result; updateWorldList(); - if (exception != null) - LOG.warning("Failed to load world list page", exception); + if (exception != null) LOG.warning("Failed to load world list page", exception); setLoading(false); }).start(); @@ -154,30 +145,28 @@ public void download() { private void installWorld(Path zipFile) { // Only accept one world file because user is required to confirm the new world name // Or too many input dialogs are popped. - Task.supplyAsync(() -> new World(zipFile)) - .whenComplete(Schedulers.javafx(), world -> { - Controllers.prompt(i18n("world.name.enter"), (name, resolve, reject) -> { - Task.runAsync(() -> world.install(savesDir, name)) - .whenComplete(Schedulers.javafx(), () -> { - resolve.run(); - refresh(); - }, e -> { - if (e instanceof FileAlreadyExistsException) - reject.accept(i18n("world.import.failed", i18n("world.import.already_exists"))); - else if (e instanceof IOException && e.getCause() instanceof InvalidPathException) - reject.accept(i18n("world.import.failed", i18n("install.new_game.malformed"))); - else - reject.accept(i18n("world.import.failed", e.getClass().getName() + ": " + e.getLocalizedMessage())); - }).start(); - }, world.getWorldName(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid)); + Task.supplyAsync(() -> new World(zipFile)).whenComplete(Schedulers.javafx(), world -> { + Controllers.prompt(i18n("world.name.enter"), (name, resolve, reject) -> { + Task.runAsync(() -> world.install(savesDir, name)).whenComplete(Schedulers.javafx(), () -> { + resolve.run(); + refresh(); }, e -> { - LOG.warning("Unable to parse world file " + zipFile, e); - Controllers.dialog(i18n("world.import.invalid")); + if (e instanceof FileAlreadyExistsException) + reject.accept(i18n("world.import.failed", i18n("world.import.already_exists"))); + else if (e instanceof IOException && e.getCause() instanceof InvalidPathException) + reject.accept(i18n("world.import.failed", i18n("install.new_game.malformed"))); + else + reject.accept(i18n("world.import.failed", e.getClass().getName() + ": " + e.getLocalizedMessage())); }).start(); + }, world.getWorldName(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid)); + }, e -> { + LOG.warning("Unable to parse world file " + zipFile, e); + Controllers.dialog(i18n("world.import.invalid")); + }).start(); } private void showManagePage(World world) { - Controllers.navigate(new WorldManagePage(world, backupsDir, profile, id)); + Controllers.navigate(new WorldManagePage(world, profile, versionId)); } public void export(World world) { @@ -197,11 +186,11 @@ public void reveal(World world) { } public void launch(World world) { - Versions.launchAndEnterWorld(profile, id, world.getFileName()); + Versions.launchAndEnterWorld(profile, versionId, world.getFileName()); } public void generateLaunchScript(World world) { - Versions.generateLaunchScriptForQuickEnterWorld(profile, id, world.getFileName()); + Versions.generateLaunchScriptForQuickEnterWorld(profile, versionId, world.getFileName()); } public BooleanProperty showAllProperty() { @@ -216,14 +205,10 @@ private final class WorldListPageSkin extends ToolbarListPageSkin initializeToolbar(WorldListPage skinnable) { - JFXCheckBox chkShowAll = new JFXCheckBox(); - chkShowAll.setText(i18n("world.show_all")); + JFXCheckBox chkShowAll = new JFXCheckBox(i18n("world.show_all")); chkShowAll.selectedProperty().bindBidirectional(skinnable.showAllProperty()); - return Arrays.asList(chkShowAll, - createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), - createToolbarButton2(i18n("world.add"), SVG.ADD, skinnable::add), - createToolbarButton2(i18n("world.download"), SVG.DOWNLOAD, skinnable::download)); + return Arrays.asList(chkShowAll, createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), createToolbarButton2(i18n("world.add"), SVG.ADD, skinnable::add), createToolbarButton2(i18n("world.download"), SVG.DOWNLOAD, skinnable::download)); } @Override @@ -240,6 +225,7 @@ private static final class WorldListCell extends ListCell { private final ImageView imageView; private final Tooltip leftTooltip; private final TwoLineListItem content; + private final JFXButton btnLaunch; public WorldListCell(WorldListPage page) { this.page = page; @@ -271,28 +257,38 @@ public WorldListCell(WorldListPage page) { root.setRight(right); right.setAlignment(Pos.CENTER_RIGHT); + btnLaunch = new JFXButton(); + right.getChildren().add(btnLaunch); + btnLaunch.getStyleClass().add("toggle-icon4"); + btnLaunch.setGraphic(SVG.ROCKET_LAUNCH.createIcon()); + FXUtils.installFastTooltip(btnLaunch, i18n("version.launch")); + btnLaunch.setOnAction(event -> { + World world = getItem(); + if (world != null && !world.isLocked()) { + page.launch(world); + } else { + updateItem(world, false); + } + }); + JFXButton btnMore = new JFXButton(); right.getChildren().add(btnMore); btnMore.getStyleClass().add("toggle-icon4"); btnMore.setGraphic(SVG.MORE_VERT.createIcon()); btnMore.setOnAction(event -> { World world = getItem(); - if (world != null) - showPopupMenu(world, JFXPopup.PopupHPosition.RIGHT, 0, root.getHeight()); + if (world != null) showPopupMenu(world, JFXPopup.PopupHPosition.RIGHT, 0, root.getHeight()); }); } this.graphic = new RipplerContainer(root); graphic.setOnMouseClicked(event -> { - if (event.getClickCount() != 1) - return; + if (event.getClickCount() != 1) return; World world = getItem(); - if (world == null) - return; + if (world == null) return; - if (event.getButton() == MouseButton.PRIMARY) - page.showManagePage(world); + if (event.getButton() == MouseButton.PRIMARY) page.showManagePage(world); else if (event.getButton() == MouseButton.SECONDARY) showPopupMenu(world, JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY()); }); @@ -315,10 +311,13 @@ protected void updateItem(World world, boolean empty) { leftTooltip.setText(world.getFile().toString()); content.setTitle(world.getWorldName() != null ? parseColorEscapes(world.getWorldName()) : ""); - if (world.getGameVersion() != null) - content.addTag(I18n.getDisplayVersion(world.getGameVersion())); - if (world.isLocked()) + if (world.getGameVersion() != null) content.addTag(I18n.getDisplayVersion(world.getGameVersion())); + if (world.isLocked()) { content.addTag(i18n("world.locked")); + btnLaunch.setDisable(true); + } else { + btnLaunch.setDisable(false); + } content.setSubtitle(i18n("world.datetime", formatDateTime(Instant.ofEpochMilli(world.getLastPlayed())))); @@ -329,55 +328,38 @@ protected void updateItem(World world, boolean empty) { // Popup Menu public void showPopupMenu(World world, JFXPopup.PopupHPosition hPosition, double initOffsetX, double initOffsetY) { + boolean worldLocked = world.isLocked(); + PopupMenu popupMenu = new PopupMenu(); JFXPopup popup = new JFXPopup(popupMenu); - if (world.getGameVersion() != null && world.getGameVersion().isAtLeast("1.20", "23w14a")) { + if (world.supportQuickPlay()) { IconedMenuItem launchItem = new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch_and_enter_world"), () -> page.launch(world), popup); - launchItem.setDisable(world.isLocked()); + launchItem.setDisable(worldLocked); popupMenu.getContent().add(launchItem); - popupMenu.getContent().addAll( - new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), () -> page.generateLaunchScript(world), popup), - new MenuSeparator() - ); + popupMenu.getContent().addAll(new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), () -> page.generateLaunchScript(world), popup), new MenuSeparator()); } popupMenu.getContent().add(new IconedMenuItem(SVG.SETTINGS, i18n("world.manage.button"), () -> page.showManagePage(world), popup)); if (ChunkBaseApp.isSupported(world)) { - popupMenu.getContent().addAll( - new MenuSeparator(), - 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) - ); - - if (world.getGameVersion() != null && world.getGameVersion().compareTo("1.13") >= 0) { - popupMenu.getContent().add(new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), - () -> ChunkBaseApp.openEndCityFinder(world), popup)); + popupMenu.getContent().addAll(new MenuSeparator(), 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)); + + if (ChunkBaseApp.supportEndCity(world)) { + popupMenu.getContent().add(new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), () -> ChunkBaseApp.openEndCityFinder(world), popup)); } } IconedMenuItem exportMenuItem = new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> page.export(world), popup); IconedMenuItem deleteMenuItem = new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> page.delete(world), popup); IconedMenuItem duplicateMenuItem = new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), () -> page.copy(world), popup); - boolean worldLocked = world.isLocked(); - Stream.of(exportMenuItem, deleteMenuItem, duplicateMenuItem) - .forEach(iconedMenuItem -> iconedMenuItem.setDisable(worldLocked)); - - popupMenu.getContent().addAll( - new MenuSeparator(), - exportMenuItem, - deleteMenuItem, - duplicateMenuItem - ); - - popupMenu.getContent().addAll( - new MenuSeparator(), - new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.world"), () -> page.reveal(world), popup) - ); + Stream.of(exportMenuItem, deleteMenuItem, duplicateMenuItem).forEach(iconedMenuItem -> iconedMenuItem.setDisable(worldLocked)); + + popupMenu.getContent().addAll(new MenuSeparator(), exportMenuItem, deleteMenuItem, duplicateMenuItem); + + popupMenu.getContent().addAll(new MenuSeparator(), new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.world"), () -> page.reveal(world), popup)); JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(this, popup); popup.show(this, vPosition, hPosition, initOffsetX, vPosition == JFXPopup.PopupVPosition.TOP ? initOffsetY : -initOffsetY); 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 2569f1042d..d7d279196a 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 @@ -19,9 +19,7 @@ import com.jfoenix.controls.JFXPopup; import javafx.application.Platform; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.*; import javafx.geometry.Insets; import javafx.scene.layout.BorderPane; import javafx.scene.layout.Priority; @@ -37,6 +35,7 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.util.ChunkBaseApp; import org.jackhuang.hmcl.util.StringUtils; +import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.nio.channels.FileChannel; @@ -50,141 +49,92 @@ */ public final class WorldManagePage extends DecoratorAnimatedPage implements DecoratorPage { - private final ObjectProperty state; private final World world; private final Path backupsDir; private final Profile profile; - private final String id; + private final String versionId; + private FileChannel sessionLockChannel; - private boolean loadFailed = false; + private final ObjectProperty state; + private boolean isFirstNavigation = true; + private final BooleanProperty refreshableProperty = new SimpleBooleanProperty(true); + private final BooleanProperty isReadOnlyProperty = new SimpleBooleanProperty(false); - private final TabHeader header; + private final TransitionPane transitionPane = new TransitionPane(); + private final TabHeader header = new TabHeader(transitionPane); private final TabHeader.Tab worldInfoTab = new TabHeader.Tab<>("worldInfoPage"); private final TabHeader.Tab worldBackupsTab = new TabHeader.Tab<>("worldBackupsPage"); private final TabHeader.Tab datapackTab = new TabHeader.Tab<>("datapackListPage"); - private final TransitionPane transitionPane = new TransitionPane(); - - private FileChannel sessionLockChannel; - - public WorldManagePage(World world, Path backupsDir, Profile profile, String id) { + public WorldManagePage(World world, Profile profile, String versionId) { this.world = world; - this.backupsDir = backupsDir; + this.backupsDir = profile.getRepository().getBackupsDirectory(versionId); this.profile = profile; - this.id = id; - - sessionLockChannel = WorldManageUIUtils.getSessionLockChannel(world); - try { - world.reloadLevelDat(); - } catch (IOException e) { - LOG.warning("Can not load world level.dat of world: " + world.getFile(), e); - loadFailed = true; - } + this.versionId = versionId; - this.worldInfoTab.setNodeSupplier(() -> new WorldInfoPage(this)); - this.worldBackupsTab.setNodeSupplier(() -> new WorldBackupsPage(this)); - this.datapackTab.setNodeSupplier(() -> new DatapackListPage(this)); + updateSessionLockChannel(); + updateWorldLevelDat(false); - this.state = new SimpleObjectProperty<>(State.fromTitle(i18n("world.manage.title", StringUtils.parseColorEscapes(world.getWorldName())))); - this.header = new TabHeader(transitionPane, worldInfoTab, worldBackupsTab); - header.select(worldInfoTab); + worldInfoTab.setNodeSupplier(() -> new WorldInfoPage(this)); + worldBackupsTab.setNodeSupplier(() -> new WorldBackupsPage(this)); + datapackTab.setNodeSupplier(() -> new DatapackListPage(this)); - setCenter(transitionPane); + this.state = new SimpleObjectProperty<>(new State(i18n("world.manage.title", StringUtils.parseColorEscapes(world.getWorldName())), null, true, true, true)); - BorderPane left = new BorderPane(); - FXUtils.setLimitWidth(left, 200); - VBox.setVgrow(left, Priority.ALWAYS); - setLeft(left); - - AdvancedListBox sideBar = new AdvancedListBox() - .addNavigationDrawerTab(header, worldInfoTab, i18n("world.info"), SVG.INFO, SVG.INFO_FILL) - .addNavigationDrawerTab(header, worldBackupsTab, i18n("world.backup"), SVG.ARCHIVE, SVG.ARCHIVE_FILL); - - if (world.getGameVersion() != null && // old game will not write game version to level.dat - world.getGameVersion().isAtLeast("1.13", "17w43a")) { - header.getTabs().add(datapackTab); - sideBar.addNavigationDrawerTab(header, datapackTab, i18n("world.datapack"), SVG.EXTENSION, SVG.EXTENSION_FILL); - } - - left.setTop(sideBar); - - AdvancedListBox toolbar = new AdvancedListBox(); - - if (world.getGameVersion() != null && world.getGameVersion().isAtLeast("1.20", "23w14a")) { - toolbar.addNavigationDrawerItem(i18n("version.launch"), SVG.ROCKET_LAUNCH, this::launch, advancedListItem -> advancedListItem.setDisable(isReadOnly())); - } + this.addEventHandler(Navigator.NavigationEvent.EXITED, this::onExited); + this.addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated); + } - if (ChunkBaseApp.isSupported(world)) { - PopupMenu chunkBasePopupMenu = new PopupMenu(); - JFXPopup chunkBasePopup = new JFXPopup(chunkBasePopupMenu); + @Override + protected @NotNull Skin createDefaultSkin() { + return new Skin(this); + } - 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) - ); + @Override + public void refresh() { + updateSessionLockChannel(); + updateWorldLevelDat(true); - if (world.getGameVersion() != null && world.getGameVersion().compareTo("1.13") >= 0) { - chunkBasePopupMenu.getContent().add( - new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), () -> ChunkBaseApp.openEndCityFinder(world), chunkBasePopup)); + for (var tab : header.getTabs()) { + if (tab.getNode() instanceof WorldRefreshable r) { + r.refresh(); } - - 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(i18n("settings.game.exploration"), SVG.FOLDER_OPEN, () -> FXUtils.openFolder(world.getFile()), null); + private void closePageForLoadingFail() { + Platform.runLater(() -> { + fireEvent(new PageCloseEvent()); + Controllers.dialog(i18n("world.load.fail"), null, MessageDialogPane.MessageType.ERROR); + }); + } - { - PopupMenu managePopupMenu = new PopupMenu(); - JFXPopup managePopup = new JFXPopup(managePopupMenu); + private void updateSessionLockChannel() { + if (sessionLockChannel == null || !sessionLockChannel.isOpen()) { + sessionLockChannel = WorldManageUIUtils.getSessionLockChannel(world); + isReadOnlyProperty.set(sessionLockChannel == null); + } + } - if (world.getGameVersion() != null && world.getGameVersion().isAtLeast("1.20", "23w14a")) { - managePopupMenu.getContent().addAll( - new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch"), this::launch, managePopup), - new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), this::generateLaunchScript, managePopup), - new MenuSeparator() - ); + private void updateWorldLevelDat(boolean PageFullyNavigated) { + try { + world.reloadLevelDat(); + } catch (IOException e) { + LOG.warning("Can not load world level.dat of world: " + world.getFile(), e); + if (PageFullyNavigated) { + closePageForLoadingFail(); + } else { + this.addEventHandler(Navigator.NavigationEvent.NAVIGATED, event -> closePageForLoadingFail()); } - - managePopupMenu.getContent().addAll( - new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> WorldManageUIUtils.export(world, sessionLockChannel), managePopup), - new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> WorldManageUIUtils.delete(world, () -> fireEvent(new PageCloseEvent()), sessionLockChannel), managePopup), - new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), () -> WorldManageUIUtils.copyWorld(world, null), managePopup) - ); - - toolbar.addNavigationDrawerItem(i18n("settings.game.management"), SVG.MENU, null, managePopupMenuItem -> - { - managePopupMenuItem.setOnAction(e -> - managePopup.show(managePopupMenuItem, - JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, - managePopupMenuItem.getWidth(), 0)); - managePopupMenuItem.setDisable(isReadOnly()); - }); - } - - BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0)); - left.setBottom(toolbar); - - this.addEventHandler(Navigator.NavigationEvent.EXITED, this::onExited); - this.addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated); } private void onNavigated(Navigator.NavigationEvent event) { - if (loadFailed) { - Platform.runLater(() -> { - fireEvent(new PageCloseEvent()); - Controllers.dialog(i18n("world.load.fail"), null, MessageDialogPane.MessageType.ERROR); - }); + if (isFirstNavigation) { + isFirstNavigation = false; return; } - if (sessionLockChannel == null || !sessionLockChannel.isOpen()) { - sessionLockChannel = WorldManageUIUtils.getSessionLockChannel(world); - } + refresh(); } public void onExited(Navigator.NavigationEvent event) { @@ -194,11 +144,24 @@ public void onExited(Navigator.NavigationEvent event) { } } + public void launch() { + fireEvent(new PageCloseEvent()); + Versions.launchAndEnterWorld(profile, versionId, world.getFileName()); + } + + public void generateLaunchScript() { + Versions.generateLaunchScriptForQuickEnterWorld(profile, versionId, world.getFileName()); + } + @Override public ReadOnlyObjectProperty stateProperty() { return state; } + public void changeStateTitle(String title) { + this.state.set(new DecoratorPage.State(title, null, true, true, true)); + } + public World getWorld() { return world; } @@ -208,15 +171,123 @@ public Path getBackupsDir() { } public boolean isReadOnly() { - return sessionLockChannel == null; + return isReadOnlyProperty.get(); } - public void launch() { - fireEvent(new PageCloseEvent()); - Versions.launchAndEnterWorld(profile, id, world.getFileName()); + public BooleanProperty readOnlyProperty() { + return isReadOnlyProperty; } - public void generateLaunchScript() { - Versions.generateLaunchScriptForQuickEnterWorld(profile, id, world.getFileName()); + @Override + public BooleanProperty refreshableProperty() { + return refreshableProperty; + } + + public static class Skin extends DecoratorAnimatedPageSkin { + + protected Skin(WorldManagePage control) { + super(control); + + setCenter(control.transitionPane); + setLeft(getSidebar()); + } + + private BorderPane getSidebar() { + BorderPane sidebar = new BorderPane(); + { + FXUtils.setLimitWidth(sidebar, 200); + VBox.setVgrow(sidebar, Priority.ALWAYS); + } + + sidebar.setTop(getTabBar()); + sidebar.setBottom(getToolBar()); + + return sidebar; + } + + private AdvancedListBox getTabBar() { + AdvancedListBox tabBar = new AdvancedListBox(); + { + getSkinnable().header.getTabs().addAll(getSkinnable().worldInfoTab, getSkinnable().worldBackupsTab); + getSkinnable().header.select(getSkinnable().worldInfoTab); + + tabBar.addNavigationDrawerTab(getSkinnable().header, getSkinnable().worldInfoTab, i18n("world.info"), SVG.INFO, SVG.INFO_FILL) + .addNavigationDrawerTab(getSkinnable().header, getSkinnable().worldBackupsTab, i18n("world.backup"), SVG.ARCHIVE, SVG.ARCHIVE_FILL); + + if (getSkinnable().world.supportDatapacks()) { + getSkinnable().header.getTabs().add(getSkinnable().datapackTab); + tabBar.addNavigationDrawerTab(getSkinnable().header, getSkinnable().datapackTab, i18n("world.datapack"), SVG.EXTENSION, SVG.EXTENSION_FILL); + } + } + + return tabBar; + } + + private AdvancedListBox getToolBar() { + AdvancedListBox toolbar = new AdvancedListBox(); + BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0)); + { + if (getSkinnable().world.supportQuickPlay()) { + toolbar.addNavigationDrawerItem(i18n("version.launch"), SVG.ROCKET_LAUNCH, () -> getSkinnable().launch(), advancedListItem -> advancedListItem.disableProperty().bind(getSkinnable().readOnlyProperty())); + } + + if (ChunkBaseApp.isSupported(getSkinnable().world)) { + PopupMenu chunkBasePopupMenu = new PopupMenu(); + JFXPopup chunkBasePopup = new JFXPopup(chunkBasePopupMenu); + + chunkBasePopupMenu.getContent().addAll( + new IconedMenuItem(SVG.EXPLORE, i18n("world.chunkbase.seed_map"), () -> ChunkBaseApp.openSeedMap(getSkinnable().world), chunkBasePopup), + new IconedMenuItem(SVG.VISIBILITY, i18n("world.chunkbase.stronghold"), () -> ChunkBaseApp.openStrongholdFinder(getSkinnable().world), chunkBasePopup), + new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(getSkinnable().world), chunkBasePopup) + ); + + if (ChunkBaseApp.supportEndCity(getSkinnable().world)) { + chunkBasePopupMenu.getContent().add( + new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), () -> ChunkBaseApp.openEndCityFinder(getSkinnable().world), chunkBasePopup)); + } + + 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(i18n("settings.game.exploration"), SVG.FOLDER_OPEN, () -> FXUtils.openFolder(getSkinnable().world.getFile())); + + { + PopupMenu managePopupMenu = new PopupMenu(); + JFXPopup managePopup = new JFXPopup(managePopupMenu); + + if (getSkinnable().world.supportQuickPlay()) { + managePopupMenu.getContent().addAll( + new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch"), () -> getSkinnable().launch(), managePopup), + new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), () -> getSkinnable().generateLaunchScript(), managePopup), + new MenuSeparator() + ); + } + + managePopupMenu.getContent().addAll( + new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> WorldManageUIUtils.export(getSkinnable().world, getSkinnable().sessionLockChannel), managePopup), + new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> WorldManageUIUtils.delete(getSkinnable().world, () -> getSkinnable().fireEvent(new PageCloseEvent()), getSkinnable().sessionLockChannel), managePopup), + new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), () -> WorldManageUIUtils.copyWorld(getSkinnable().world, null), managePopup) + ); + + toolbar.addNavigationDrawerItem(i18n("settings.game.management"), SVG.MENU, null, managePopupMenuItem -> + { + managePopupMenuItem.setOnAction(e -> + managePopup.show(managePopupMenuItem, + JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, + managePopupMenuItem.getWidth(), 0)); + managePopupMenuItem.disableProperty().bind(getSkinnable().readOnlyProperty()); + }); + } + } + return toolbar; + } + } + + public interface WorldRefreshable { + void refresh(); } } 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 761144ce4d..bd7d3a2514 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 @@ -144,7 +144,7 @@ public static FileChannel getSessionLockChannel(World world) { FileChannel lock = world.lock(); LOG.info("Acquired lock on world " + world.getFileName()); return lock; - } catch (IOException ignored) { + } catch (WorldLockedException ignored) { return null; } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/ChunkBaseApp.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/ChunkBaseApp.java index 8556161bda..dca5695012 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/ChunkBaseApp.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/ChunkBaseApp.java @@ -28,6 +28,7 @@ public final class ChunkBaseApp { private static final String CHUNK_BASE_URL = "https://www.chunkbase.com"; private static final GameVersionNumber MIN_GAME_VERSION = GameVersionNumber.asGameVersion("1.7"); + private static final GameVersionNumber MIN_END_CITY_VERSION = GameVersionNumber.asGameVersion("1.13"); private static final String[] SEED_MAP_GAME_VERSIONS = { "1.21.9", "1.21.6", "1.21.5", "1.21.4", "1.21.2", "1.21", "1.20", @@ -52,6 +53,11 @@ public static boolean isSupported(@NotNull World world) { world.getGameVersion().compareTo(MIN_GAME_VERSION) >= 0; } + public static boolean supportEndCity(@NotNull World world) { + return world.getSeed() != null && world.getGameVersion() != null && + world.getGameVersion().compareTo(MIN_END_CITY_VERSION) >= 0; + } + public static ChunkBaseApp newBuilder(String app, long seed) { return new ChunkBaseApp(new StringBuilder(CHUNK_BASE_URL).append("/apps/").append(app).append("#seed=").append(seed)); } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 7b2439aa8a..a89c72418f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1115,6 +1115,7 @@ datapack=Datapacks datapack.add=Install Datapack datapack.choose_datapack=Choose datapack to import datapack.extension=Datapack +datapack.reload.toast=Minecraft is running, please use the /reload command to reload the data pack datapack.title=World [%s] - Datapacks web.failed=Failed to load page @@ -1183,6 +1184,7 @@ world.info.last_played=Last Played world.info.generate_features=Generate Structures world.info.player=Player Information world.info.player.food_level=Hunger Level +world.info.player.food_saturation_level=Saturation world.info.player.game_type=Game Mode world.info.player.game_type.adventure=Adventure world.info.player.game_type.creative=Creative @@ -1195,8 +1197,9 @@ world.info.player.location=Location world.info.player.spawn=Spawn Location world.info.player.xp_level=Experience Level world.info.random_seed=Seed -world.info.time=Game Time -world.info.time.format=%s days +world.info.spawn=World Spawn Location +world.info.time=Played Time +world.info.time.format=%dd %dh %dm world.load.fail=Failed to load world world.locked=In use world.locked.failed=The world is currently in use. Please close the game and try again. diff --git a/HMCL/src/main/resources/assets/lang/I18N_ar.properties b/HMCL/src/main/resources/assets/lang/I18N_ar.properties index 0035318041..87969021a4 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ar.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ar.properties @@ -1153,8 +1153,6 @@ world.info.player.location=الموقع world.info.player.spawn=موقع الظهور world.info.player.xp_level=مستوى الخبرة world.info.random_seed=البذرة -world.info.time=وقت اللعبة -world.info.time.format=%s أيام world.locked=قيد الاستخدام world.locked.failed=العالم قيد الاستخدام حاليًا. يرجى إغلاق اللعبة والمحاولة مرة أخرى. world.manage=العوالم diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index b61310854a..92e93dbaa2 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -1160,8 +1160,6 @@ world.info.player.location=Ubicación world.info.player.spawn=Ubicación de desove world.info.player.xp_level=Nivel de experiencia world.info.random_seed=Semilla -world.info.time=Tiempo de juego -world.info.time.format=%s días world.locked=En uso world.locked.failed=El mundo está actualmente en uso. Por favor, cierra el juego e inténtalo de nuevo. world.manage=Mundos diff --git a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties index 4208e23ecf..6332a05ff9 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties @@ -945,8 +945,6 @@ world.info.player.location=所 world.info.player.spawn=床/復生錨之所 world.info.player.xp_level=經驗之層 world.info.random_seed=種 -world.info.time=戲之時辰 -world.info.time.format=%s 日 world.locked=見用 world.manage=司生界 world.manage.button=司生界 diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index 6873232d75..5964481a07 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -1152,8 +1152,6 @@ world.info.player.location=Расположение world.info.player.spawn=Точка возрождения world.info.player.xp_level=Уровень опыта world.info.random_seed=Ключ генератора мира -world.info.time=Время игры -world.info.time.format=%s дн. world.locked=В эксплуатации world.locked.failed=В настоящее время мир находится в эксплуатации. Закройте игру и попробуйте снова. world.manage=Миры diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index 30949bc442..71088de9d9 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -1097,8 +1097,6 @@ world.info.player.location=Місцезнаходження world.info.player.spawn=Місце появи world.info.player.xp_level=Рівень досвіду world.info.random_seed=Насіння -world.info.time=Час гри -world.info.time.format=%s днів world.locked=Використовується world.locked.failed=Світ наразі використовується. Закрийте гру та спробуйте знову. world.manage=Світи diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 623bcb8849..aaafc8baaa 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -903,6 +903,7 @@ datapack=資料包 datapack.add=新增資料包 datapack.choose_datapack=選取要匯入的資料包壓縮檔 datapack.extension=資料包 +datapack.reload.toast=Minecraft 正在執行,請使用 /reload 指令重新載入資料包 datapack.title=世界 [%s] - 資料包 web.failed=載入頁面失敗 @@ -970,6 +971,7 @@ world.info.last_played=上一次遊戲時間 world.info.generate_features=生成建築 world.info.player=玩家資訊 world.info.player.food_level=饑餓值 +world.info.player.food_saturation_level=飽食度 world.info.player.game_type=遊戲模式 world.info.player.game_type.adventure=冒險 world.info.player.game_type.creative=創造 @@ -982,8 +984,9 @@ world.info.player.location=位置 world.info.player.spawn=床/重生錨位置 world.info.player.xp_level=經驗等級 world.info.random_seed=種子碼 -world.info.time=遊戲內時間 -world.info.time.format=%s 天 +world.info.spawn=世界重生點 +world.info.time=遊戲時間 +world.info.time.format=%d 天 %d 小時 %d 分鐘 world.load.fail=世界載入失敗 world.locked=使用中 world.locked.failed=該世界正在使用中,請關閉遊戲後重試。 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 eac122af6b..6b185ff23a 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -907,6 +907,7 @@ datapack=数据包 datapack.add=添加数据包 datapack.choose_datapack=选择要导入的数据包压缩包 datapack.extension=数据包 +datapack.reload.toast=Minecraft 正在运行,请使用 /reload 命令重新加载数据包 datapack.title=世界 [%s] - 数据包 web.failed=加载页面失败 @@ -975,6 +976,7 @@ world.info.last_played=上一次游戏时间 world.info.generate_features=生成建筑 world.info.player=玩家信息 world.info.player.food_level=饥饿值 +world.info.player.food_saturation_level=饱和度 world.info.player.game_type=游戏模式 world.info.player.game_type.adventure=冒险 world.info.player.game_type.creative=创造 @@ -987,8 +989,9 @@ world.info.player.location=位置 world.info.player.spawn=床/重生锚位置 world.info.player.xp_level=经验等级 world.info.random_seed=种子 -world.info.time=游戏内时间 -world.info.time.format=%s 天 +world.info.spawn=世界出生点 +world.info.time=游戏时长 +world.info.time.format=%d 天 %d 小时 %d 分钟 world.load.fail=世界加载失败 world.locked=使用中 world.locked.failed=该世界正在使用中,请关闭游戏后重试。 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 24943caf07..be84e4325b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java @@ -34,7 +34,6 @@ import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.util.List; -import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; @@ -47,7 +46,6 @@ public final class World { private String fileName; private CompoundTag levelData; private Image icon; - private boolean isLocked; private Path levelDataPath; public World(Path file) throws IOException { @@ -144,7 +142,19 @@ public Image getIcon() { } public boolean isLocked() { - return isLocked; + return isLocked(getSessionLockFile()); + } + + public boolean supportDatapacks() { + return getGameVersion() != null && getGameVersion().isAtLeast("1.13", "17w43a"); + } + + public boolean supportQuickPlay() { + return getGameVersion() != null && getGameVersion().isAtLeast("1.20", "23w14a"); + } + + public static boolean supportQuickPlay(GameVersionNumber gameVersionNumber) { + return gameVersionNumber != null && gameVersionNumber.isAtLeast("1.20", "23w14a"); } private void loadFromDirectory() throws IOException { @@ -153,9 +163,11 @@ private void loadFromDirectory() throws IOException { if (!Files.exists(levelDat)) { // version 20w14infinite levelDat = file.resolve("special_level.dat"); } + if (!Files.exists(levelDat)) { + throw new IOException("Not a valid world directory since level.dat or special_level.dat cannot be found."); + } loadAndCheckLevelDat(levelDat); this.levelDataPath = levelDat; - isLocked = isLocked(getSessionLockFile()); Path iconFile = file.resolve("icon.png"); if (Files.isRegularFile(iconFile)) { @@ -177,7 +189,6 @@ private void loadFromZipImpl(Path root) throws IOException { if (!Files.exists(levelDat)) { throw new IOException("Not a valid world zip file since level.dat or special_level.dat cannot be found."); } - loadAndCheckLevelDat(levelDat); Path iconFile = root.resolve("icon.png"); @@ -193,10 +204,9 @@ private void loadFromZipImpl(Path root) throws IOException { } private void loadFromZip() throws IOException { - isLocked = false; try (FileSystem fs = CompressingUtils.readonly(file).setAutoDetectEncoding(true).build()) { - Path cur = fs.getPath("/level.dat"); - if (Files.isRegularFile(cur)) { + Path levelDatPath = fs.getPath("/level.dat"); + if (Files.isRegularFile(levelDatPath)) { fileName = FileUtils.getName(file); loadFromZipImpl(fs.getPath("/")); return; @@ -230,6 +240,8 @@ public void reloadLevelDat() throws IOException { } } + // The rename method is used to rename temporary world object during installation and copying, + // so there is no need to modify the `file` field. public void rename(String newName) throws IOException { if (!Files.isDirectory(file)) throw new IOException("Not a valid world directory"); @@ -257,14 +269,14 @@ public void install(Path savesDir, String name) throws IOException { if (Files.isRegularFile(file)) { try (FileSystem fs = CompressingUtils.readonly(file).setAutoDetectEncoding(true).build()) { - Path cur = fs.getPath("/level.dat"); - if (Files.isRegularFile(cur)) { + Path levelDatPath = fs.getPath("/level.dat"); + if (Files.isRegularFile(levelDatPath)) { fileName = FileUtils.getName(file); new Unzipper(file, worldDir).unzip(); } else { try (Stream stream = Files.list(fs.getPath("/"))) { - List subDirs = stream.collect(Collectors.toList()); + List subDirs = stream.toList(); if (subDirs.size() != 1) { throw new IOException("World zip malformed"); } @@ -347,8 +359,8 @@ public void writeLevelDat(CompoundTag nbt) throws IOException { private static CompoundTag parseLevelDat(Path path) throws IOException { try (InputStream is = new GZIPInputStream(Files.newInputStream(path))) { Tag nbt = NBTIO.readTag(is); - if (nbt instanceof CompoundTag) - return (CompoundTag) nbt; + if (nbt instanceof CompoundTag compoundTag) + return compoundTag; else throw new IOException("level.dat malformed"); } @@ -367,21 +379,21 @@ private static boolean isLocked(Path sessionLockFile) { } } - public static Stream getWorlds(Path savesDir) { - try { - if (Files.exists(savesDir)) { - return Files.list(savesDir).flatMap(world -> { + public static List getWorlds(Path savesDir) { + if (Files.exists(savesDir)) { + try (Stream stream = Files.list(savesDir)) { + return stream.flatMap(world -> { try { return Stream.of(new World(world.toAbsolutePath())); } catch (IOException e) { LOG.warning("Failed to read world " + world, e); return Stream.empty(); } - }); + }).toList(); + } catch (IOException e) { + LOG.warning("Failed to read saves", e); } - } catch (IOException e) { - LOG.warning("Failed to read saves", e); } - return Stream.empty(); + return List.of(); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java index 777be6b75e..2efd47fa93 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java @@ -309,7 +309,7 @@ private Command generateCommandLine(Path nativeFolder) throws IOException { try { ServerAddress parsed = ServerAddress.parse(address); - if (GameVersionNumber.asGameVersion(gameVersion).isAtLeast("1.20", "23w14a")) { + if (World.supportQuickPlay(GameVersionNumber.asGameVersion(gameVersion))) { res.add("--quickPlayMultiplayer"); res.add(parsed.getPort() >= 0 ? address : parsed.getHost() + ":25565"); } else { @@ -322,11 +322,11 @@ private Command generateCommandLine(Path nativeFolder) throws IOException { LOG.warning("Invalid server address: " + address, e); } } else if (options.getQuickPlayOption() instanceof QuickPlayOption.SinglePlayer singlePlayer - && GameVersionNumber.asGameVersion(gameVersion).isAtLeast("1.20", "23w14a")) { + && World.supportQuickPlay(GameVersionNumber.asGameVersion(gameVersion))) { res.add("--quickPlaySingleplayer"); res.add(singlePlayer.worldFolderName()); } else if (options.getQuickPlayOption() instanceof QuickPlayOption.Realm realm - && GameVersionNumber.asGameVersion(gameVersion).isAtLeast("1.20", "23w14a")) { + && World.supportQuickPlay(GameVersionNumber.asGameVersion(gameVersion))) { res.add("--quickPlayRealms"); res.add(realm.realmID()); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java index 88670f2650..5322e5b75b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java @@ -280,6 +280,15 @@ public static Double toDoubleOrNull(Object string) { } } + public static Float toFloatOrNull(Object string) { + try { + if (string == null) return null; + return Float.parseFloat(string.toString()); + } catch (NumberFormatException e) { + return null; + } + } + /** * Find the first non-null reference in given list. *