feat: 优化世界管理界面和世界信息界面 (#4823)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.github.steveice10.opennbt.tag.builtin.*;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXComboBox;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
@@ -28,27 +29,36 @@ import javafx.scene.Cursor;
|
||||
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.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.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 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.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
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
|
||||
@@ -58,6 +68,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();
|
||||
@@ -80,7 +92,7 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
if (!Files.isDirectory(world.getFile()))
|
||||
throw new IOException("Not a valid world directory");
|
||||
|
||||
return world.readLevelDat();
|
||||
return world.getLevelData();
|
||||
}
|
||||
|
||||
private void updateControls() {
|
||||
@@ -103,98 +115,132 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
{
|
||||
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();
|
||||
setRightTextField(worldNamePane, worldNameField, 200);
|
||||
|
||||
Label worldNameLabel = new Label();
|
||||
FXUtils.copyOnDoubleClick(worldNameLabel);
|
||||
worldNameLabel.setText(world.getWorldName());
|
||||
BorderPane.setAlignment(worldNameLabel, Pos.CENTER_RIGHT);
|
||||
worldNamePane.setRight(worldNameLabel);
|
||||
Tag tag = dataTag.get("LevelName");
|
||||
if (tag instanceof StringTag stringTag) {
|
||||
worldNameField.setText(stringTag.getValue());
|
||||
|
||||
worldNameField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
try {
|
||||
world.setWorldName(newValue);
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to set world name", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
worldNameField.setDisable(true);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
if (world.getGameVersion() != null)
|
||||
gameVersionLabel.setText(world.getGameVersion().toNormalizedString());
|
||||
BorderPane.setAlignment(gameVersionLabel, Pos.CENTER_RIGHT);
|
||||
gameVersionPane.setRight(gameVersionLabel);
|
||||
setRightTextLabel(gameVersionPane, gameVersionLabel, () -> world.getGameVersion() == null ? "" : world.getGameVersion().toNormalizedString());
|
||||
}
|
||||
|
||||
BorderPane randomSeedPane = new BorderPane();
|
||||
BorderPane iconPane = new BorderPane();
|
||||
{
|
||||
setLeftLabel(iconPane, "world.icon");
|
||||
|
||||
HBox left = new HBox(8);
|
||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||
left.setAlignment(Pos.CENTER_LEFT);
|
||||
randomSeedPane.setLeft(left);
|
||||
Runnable onClickAction = () -> Controllers.confirm(
|
||||
i18n("world.icon.change.tip"), i18n("world.icon.change"), MessageDialogPane.MessageType.INFO,
|
||||
this::changeWorldIcon,
|
||||
null
|
||||
);
|
||||
|
||||
Label label = new Label(i18n("world.info.random_seed"));
|
||||
FXUtils.limitSize(iconImageView, 32, 32);
|
||||
{
|
||||
iconImageView.setImage(world.getIcon() == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : world.getIcon());
|
||||
}
|
||||
|
||||
JFXButton editIconButton = new JFXButton();
|
||||
JFXButton resetIconButton = new JFXButton();
|
||||
{
|
||||
editIconButton.setGraphic(SVG.EDIT.createIcon(20));
|
||||
editIconButton.setDisable(worldManagePage.isReadOnly());
|
||||
FXUtils.onClicked(editIconButton, onClickAction);
|
||||
FXUtils.installFastTooltip(editIconButton, i18n("button.edit"));
|
||||
editIconButton.getStyleClass().add("toggle-icon4");
|
||||
|
||||
resetIconButton.setGraphic(SVG.RESTORE.createIcon(20));
|
||||
resetIconButton.setDisable(worldManagePage.isReadOnly());
|
||||
FXUtils.onClicked(resetIconButton, this::clearWorldIcon);
|
||||
FXUtils.installFastTooltip(resetIconButton, i18n("button.reset"));
|
||||
resetIconButton.getStyleClass().add("toggle-icon4");
|
||||
}
|
||||
|
||||
HBox hBox = new HBox(8);
|
||||
hBox.setAlignment(Pos.CENTER_LEFT);
|
||||
hBox.getChildren().addAll(iconImageView, editIconButton, resetIconButton);
|
||||
|
||||
iconPane.setRight(hBox);
|
||||
}
|
||||
|
||||
BorderPane seedPane = new BorderPane();
|
||||
{
|
||||
setLeftLabel(seedPane, "world.info.random_seed");
|
||||
|
||||
SimpleBooleanProperty visibility = new SimpleBooleanProperty();
|
||||
StackPane visibilityButton = new StackPane();
|
||||
visibilityButton.setCursor(Cursor.HAND);
|
||||
FXUtils.setLimitWidth(visibilityButton, 12);
|
||||
FXUtils.setLimitHeight(visibilityButton, 12);
|
||||
FXUtils.onClicked(visibilityButton, () -> visibility.set(!visibility.get()));
|
||||
|
||||
left.getChildren().setAll(label, visibilityButton);
|
||||
|
||||
Label randomSeedLabel = new Label();
|
||||
FXUtils.copyOnDoubleClick(randomSeedLabel);
|
||||
BorderPane.setAlignment(randomSeedLabel, Pos.CENTER_RIGHT);
|
||||
randomSeedPane.setRight(randomSeedLabel);
|
||||
|
||||
Tag tag = worldGenSettings != null ? worldGenSettings.get("seed") : dataTag.get("RandomSeed");
|
||||
if (tag instanceof LongTag) {
|
||||
randomSeedLabel.setText(tag.getValue().toString());
|
||||
{
|
||||
visibilityButton.setCursor(Cursor.HAND);
|
||||
visibilityButton.setAlignment(Pos.BOTTOM_RIGHT);
|
||||
FXUtils.setLimitWidth(visibilityButton, 12);
|
||||
FXUtils.setLimitHeight(visibilityButton, 12);
|
||||
FXUtils.onClicked(visibilityButton, () -> visibility.set(!visibility.get()));
|
||||
}
|
||||
|
||||
BoxBlur blur = new BoxBlur();
|
||||
blur.setIterations(3);
|
||||
FXUtils.onChangeAndOperate(visibility, isVisibility -> {
|
||||
SVG icon = isVisibility ? SVG.VISIBILITY : SVG.VISIBILITY_OFF;
|
||||
visibilityButton.getChildren().setAll(icon.createIcon(12));
|
||||
randomSeedLabel.setEffect(isVisibility ? null : blur);
|
||||
});
|
||||
Label seedLabel = new Label();
|
||||
{
|
||||
FXUtils.copyOnDoubleClick(seedLabel);
|
||||
seedLabel.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
seedLabel.setText(world.getSeed() != null ? world.getSeed().toString() : "");
|
||||
|
||||
BoxBlur blur = new BoxBlur();
|
||||
blur.setIterations(3);
|
||||
FXUtils.onChangeAndOperate(visibility, isVisibility -> {
|
||||
SVG icon = isVisibility ? SVG.VISIBILITY : SVG.VISIBILITY_OFF;
|
||||
visibilityButton.getChildren().setAll(icon.createIcon(12));
|
||||
seedLabel.setEffect(isVisibility ? null : blur);
|
||||
});
|
||||
}
|
||||
|
||||
HBox right = new HBox(8);
|
||||
{
|
||||
BorderPane.setAlignment(right, Pos.CENTER_RIGHT);
|
||||
right.getChildren().setAll(visibilityButton, seedLabel);
|
||||
seedPane.setRight(right);
|
||||
}
|
||||
}
|
||||
|
||||
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, () -> formatDateTime(Instant.ofEpochMilli(world.getLastPlayed())));
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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();
|
||||
@@ -203,21 +249,7 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
allowCheatsButton.setDisable(worldManagePage.isReadOnly());
|
||||
Tag tag = dataTag.get("allowCommands");
|
||||
|
||||
if (tag instanceof ByteTag) {
|
||||
ByteTag byteTag = (ByteTag) tag;
|
||||
byte value = byteTag.getValue();
|
||||
if (value == 0 || value == 1) {
|
||||
allowCheatsButton.setSelected(value == 1);
|
||||
allowCheatsButton.selectedProperty().addListener((o, oldValue, newValue) -> {
|
||||
byteTag.setValue(newValue ? (byte) 1 : (byte) 0);
|
||||
saveLevelDat();
|
||||
});
|
||||
} else {
|
||||
allowCheatsButton.setDisable(true);
|
||||
}
|
||||
} else {
|
||||
allowCheatsButton.setDisable(true);
|
||||
}
|
||||
checkTagAndSetListener(tag, allowCheatsButton);
|
||||
}
|
||||
|
||||
OptionToggleButton generateFeaturesButton = new OptionToggleButton();
|
||||
@@ -226,28 +258,12 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
generateFeaturesButton.setDisable(worldManagePage.isReadOnly());
|
||||
Tag tag = worldGenSettings != null ? worldGenSettings.get("generate_features") : dataTag.get("MapFeatures");
|
||||
|
||||
if (tag instanceof ByteTag) {
|
||||
ByteTag byteTag = (ByteTag) tag;
|
||||
byte value = byteTag.getValue();
|
||||
if (value == 0 || value == 1) {
|
||||
generateFeaturesButton.setSelected(value == 1);
|
||||
generateFeaturesButton.selectedProperty().addListener((o, oldValue, newValue) -> {
|
||||
byteTag.setValue(newValue ? (byte) 1 : (byte) 0);
|
||||
saveLevelDat();
|
||||
});
|
||||
} else {
|
||||
generateFeaturesButton.setDisable(true);
|
||||
}
|
||||
} else {
|
||||
generateFeaturesButton.setDisable(true);
|
||||
}
|
||||
checkTagAndSetListener(tag, generateFeaturesButton);
|
||||
}
|
||||
|
||||
BorderPane difficultyPane = new BorderPane();
|
||||
{
|
||||
Label label = new Label(i18n("world.info.difficulty"));
|
||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
difficultyPane.setLeft(label);
|
||||
setLeftLabel(difficultyPane, "world.info.difficulty");
|
||||
|
||||
JFXComboBox<Difficulty> difficultyBox = new JFXComboBox<>(Difficulty.items);
|
||||
difficultyBox.setDisable(worldManagePage.isReadOnly());
|
||||
@@ -255,8 +271,7 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
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,86 +289,93 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
}
|
||||
}
|
||||
|
||||
OptionToggleButton difficultyLockPane = new OptionToggleButton();
|
||||
{
|
||||
difficultyLockPane.setTitle(i18n("world.info.difficulty_lock"));
|
||||
difficultyLockPane.setDisable(worldManagePage.isReadOnly());
|
||||
|
||||
Tag tag = dataTag.get("DifficultyLocked");
|
||||
checkTagAndSetListener(tag, difficultyLockPane);
|
||||
}
|
||||
|
||||
basicInfo.getContent().setAll(
|
||||
worldNamePane, gameVersionPane, randomSeedPane, lastPlayedPane, timePane,
|
||||
allowCheatsButton, generateFeaturesButton, difficultyPane);
|
||||
worldNamePane, gameVersionPane, iconPane, seedPane, lastPlayedPane, timePane,
|
||||
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();
|
||||
{
|
||||
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);
|
||||
|
||||
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();
|
||||
{
|
||||
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);
|
||||
|
||||
Tag tag = player.get("LastDeathLocation");
|
||||
if (tag instanceof CompoundTag) {
|
||||
Dimension dim = Dimension.of(((CompoundTag) tag).get("dimension"));
|
||||
if (dim != null) {
|
||||
String posString = dim.formatPosition(((CompoundTag) tag).get("pos"));
|
||||
if (posString != null)
|
||||
lastDeathLocationLabel.setText(posString);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
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) {
|
||||
Tag x = player.get("SpawnX");
|
||||
Tag y = player.get("SpawnY");
|
||||
Tag z = player.get("SpawnZ");
|
||||
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 (x instanceof IntTag && y instanceof IntTag && z instanceof IntTag)
|
||||
spawnLabel.setText(dim.formatPosition(((IntTag) x).getValue(), ((IntTag) y).getValue(), ((IntTag) z).getValue()));
|
||||
}
|
||||
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
|
||||
// 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 "";
|
||||
});
|
||||
}
|
||||
|
||||
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<GameType> gameTypeBox = new JFXComboBox<>(GameType.items);
|
||||
gameTypeBox.setDisable(worldManagePage.isReadOnly());
|
||||
@@ -364,8 +386,7 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
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);
|
||||
@@ -395,33 +416,13 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
|
||||
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 floatTag = (FloatTag) tag;
|
||||
healthField.setText(new DecimalFormat("#").format(floatTag.getValue().floatValue()));
|
||||
|
||||
healthField.textProperty().addListener((o, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
try {
|
||||
floatTag.setValue(Float.parseFloat(newValue));
|
||||
saveLevelDat();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
});
|
||||
FXUtils.setValidateWhileTextChanged(healthField, true);
|
||||
healthField.setValidators(new DoubleValidator(i18n("input.number"), true));
|
||||
if (tag instanceof FloatTag floatTag) {
|
||||
setTagAndTextField(floatTag, healthField);
|
||||
} else {
|
||||
healthField.setDisable(true);
|
||||
}
|
||||
@@ -429,33 +430,13 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
|
||||
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 intTag = (IntTag) tag;
|
||||
foodLevelField.setText(String.valueOf(intTag.getValue()));
|
||||
|
||||
foodLevelField.textProperty().addListener((o, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
try {
|
||||
intTag.setValue(Integer.parseInt(newValue));
|
||||
saveLevelDat();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
});
|
||||
FXUtils.setValidateWhileTextChanged(foodLevelField, true);
|
||||
foodLevelField.setValidators(new NumberValidator(i18n("input.number"), true));
|
||||
if (tag instanceof IntTag intTag) {
|
||||
setTagAndTextField(intTag, foodLevelField);
|
||||
} else {
|
||||
foodLevelField.setDisable(true);
|
||||
}
|
||||
@@ -463,33 +444,13 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
|
||||
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 intTag = (IntTag) tag;
|
||||
xpLevelField.setText(String.valueOf(intTag.getValue()));
|
||||
|
||||
xpLevelField.textProperty().addListener((o, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
try {
|
||||
intTag.setValue(Integer.parseInt(newValue));
|
||||
saveLevelDat();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
});
|
||||
FXUtils.setValidateWhileTextChanged(xpLevelField, true);
|
||||
xpLevelField.setValidators(new NumberValidator(i18n("input.number"), true));
|
||||
if (tag instanceof IntTag intTag) {
|
||||
setTagAndTextField(intTag, xpLevelField);
|
||||
} else {
|
||||
xpLevelField.setDisable(true);
|
||||
}
|
||||
@@ -504,6 +465,88 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
}
|
||||
}
|
||||
|
||||
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.isReadOnly());
|
||||
textField.setPrefWidth(perfWidth);
|
||||
textField.setAlignment(Pos.CENTER_RIGHT);
|
||||
borderPane.setRight(textField);
|
||||
}
|
||||
|
||||
private void setRightTextLabel(BorderPane borderPane, Label label, Callable<String> setNameCall) {
|
||||
FXUtils.copyOnDoubleClick(label);
|
||||
BorderPane.setAlignment(label, Pos.CENTER_RIGHT);
|
||||
try {
|
||||
label.setText(setNameCall.call());
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Exception happened when setting name", e);
|
||||
}
|
||||
borderPane.setRight(label);
|
||||
}
|
||||
|
||||
private void checkTagAndSetListener(Tag tag, OptionToggleButton toggleButton) {
|
||||
if (tag instanceof ByteTag byteTag) {
|
||||
byte value = byteTag.getValue();
|
||||
if (value == 0 || value == 1) {
|
||||
toggleButton.setSelected(value == 1);
|
||||
toggleButton.selectedProperty().addListener((o, oldValue, newValue) -> {
|
||||
try {
|
||||
byteTag.setValue((byte) (newValue ? 1 : 0));
|
||||
saveLevelDat();
|
||||
} catch (Exception e) {
|
||||
toggleButton.setSelected(oldValue);
|
||||
LOG.warning("Exception happened when saving level.dat", e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
toggleButton.setDisable(true);
|
||||
}
|
||||
} else {
|
||||
toggleButton.setDisable(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void setTagAndTextField(IntTag intTag, JFXTextField jfxTextField) {
|
||||
jfxTextField.setText(String.valueOf(intTag.getValue()));
|
||||
|
||||
jfxTextField.textProperty().addListener((o, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
try {
|
||||
intTag.setValue(Integer.parseInt(newValue));
|
||||
saveLevelDat();
|
||||
} catch (Exception e) {
|
||||
jfxTextField.setText(oldValue);
|
||||
LOG.warning("Exception happened when saving level.dat", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
FXUtils.setValidateWhileTextChanged(jfxTextField, true);
|
||||
jfxTextField.setValidators(new NumberValidator(i18n("input.number"), true));
|
||||
}
|
||||
|
||||
private void setTagAndTextField(FloatTag floatTag, JFXTextField jfxTextField) {
|
||||
jfxTextField.setText(new DecimalFormat("#").format(floatTag.getValue().floatValue()));
|
||||
|
||||
jfxTextField.textProperty().addListener((o, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
try {
|
||||
floatTag.setValue(Float.parseFloat(newValue));
|
||||
saveLevelDat();
|
||||
} catch (Exception e) {
|
||||
jfxTextField.setText(oldValue);
|
||||
LOG.warning("Exception happened when saving level.dat", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
FXUtils.setValidateWhileTextChanged(jfxTextField, true);
|
||||
jfxTextField.setValidators(new DoubleValidator(i18n("input.number"), true));
|
||||
}
|
||||
|
||||
private void saveLevelDat() {
|
||||
LOG.info("Saving level.dat of world " + world.getWorldName());
|
||||
try {
|
||||
@@ -513,54 +556,34 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
switch (((IntTag) tag).getValue()) {
|
||||
case 0:
|
||||
return OVERWORLD;
|
||||
case 1:
|
||||
return THE_NETHER;
|
||||
case 2:
|
||||
return THE_END;
|
||||
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);
|
||||
}
|
||||
if (tag instanceof IntTag intTag) {
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private Dimension(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
String formatPosition(Tag tag) {
|
||||
if (tag instanceof ListTag) {
|
||||
ListTag listTag = (ListTag) tag;
|
||||
if (listTag.size() != 3)
|
||||
return null;
|
||||
if (tag instanceof ListTag listTag && listTag.size() == 3) {
|
||||
|
||||
Tag x = listTag.get(0);
|
||||
Tag y = listTag.get(1);
|
||||
@@ -575,8 +598,7 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
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);
|
||||
@@ -609,7 +631,7 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
static final ObservableList<Difficulty> 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
|
||||
@@ -633,4 +655,51 @@ public final class WorldInfoPage extends SpinnerPane {
|
||||
return i18n("world.info.player.game_type." + name().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
|
||||
private void changeWorldIcon() {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
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());
|
||||
if (file == null) return;
|
||||
|
||||
Image image;
|
||||
try {
|
||||
image = FXUtils.loadImage(file.toPath());
|
||||
} 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);
|
||||
return;
|
||||
}
|
||||
if ((int) image.getWidth() == 64 && (int) image.getHeight() == 64) {
|
||||
Path output = world.getFile().resolve("icon.png");
|
||||
saveImage(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) {
|
||||
Image oldImage = iconImageView.getImage();
|
||||
try {
|
||||
PNGJavaFXUtils.writeImage(image, path);
|
||||
iconImageView.setImage(image);
|
||||
Controllers.showToast(i18n("world.icon.change.succeed.toast"));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to save world icon " + e.getMessage());
|
||||
iconImageView.setImage(oldImage);
|
||||
}
|
||||
}
|
||||
|
||||
private void clearWorldIcon() {
|
||||
Path output = world.getFile().resolve("icon.png");
|
||||
try {
|
||||
Files.deleteIfExists(output);
|
||||
iconImageView.setImage(FXUtils.newBuiltinImage("/assets/img/unknown_server.png"));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to delete world icon " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,23 +19,13 @@ package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
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.setting.Profile;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.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;
|
||||
@@ -61,34 +51,15 @@ public final class WorldListItem extends Control {
|
||||
}
|
||||
|
||||
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 copy() {
|
||||
WorldManageUIUtils.copyWorld(world, parent::refresh);
|
||||
}
|
||||
|
||||
public void reveal() {
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.jackhuang.hmcl.util.ChunkBaseApp;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition;
|
||||
import static org.jackhuang.hmcl.util.StringUtils.parseColorEscapes;
|
||||
@@ -143,11 +144,24 @@ public final class WorldListItemSkin extends SkinBase<WorldListItem> {
|
||||
}
|
||||
}
|
||||
|
||||
IconedMenuItem exportMenuItem = new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), item::export, popup);
|
||||
IconedMenuItem deleteMenuItem = new IconedMenuItem(SVG.DELETE, i18n("world.delete"), item::delete, popup);
|
||||
IconedMenuItem duplicateMenuItem = new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), item::copy, popup);
|
||||
boolean worldLocked = world.isLocked();
|
||||
Stream.of(exportMenuItem, deleteMenuItem, duplicateMenuItem)
|
||||
.forEach(iconedMenuItem -> iconedMenuItem.setDisable(worldLocked));
|
||||
|
||||
popupMenu.getContent().addAll(
|
||||
new MenuSeparator(),
|
||||
new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), item::export, popup),
|
||||
new IconedMenuItem(SVG.DELETE, i18n("world.delete"), item::delete, popup),
|
||||
new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.world"), item::reveal, popup));
|
||||
exportMenuItem,
|
||||
deleteMenuItem,
|
||||
duplicateMenuItem
|
||||
);
|
||||
|
||||
popupMenu.getContent().addAll(
|
||||
new MenuSeparator(),
|
||||
new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.world"), item::reveal, popup)
|
||||
);
|
||||
|
||||
JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup);
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
@@ -27,6 +28,7 @@ import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.game.World;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
@@ -34,6 +36,7 @@ import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.ChunkBaseApp;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
@@ -53,6 +56,8 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
private final Profile profile;
|
||||
private final String id;
|
||||
|
||||
private boolean loadFailed = false;
|
||||
|
||||
private final TabHeader header;
|
||||
private final TabHeader.Tab<WorldInfoPage> worldInfoTab = new TabHeader.Tab<>("worldInfoPage");
|
||||
private final TabHeader.Tab<WorldBackupsPage> worldBackupsTab = new TabHeader.Tab<>("worldBackupsPage");
|
||||
@@ -65,25 +70,25 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
public WorldManagePage(World world, Path backupsDir, Profile profile, String id) {
|
||||
this.world = world;
|
||||
this.backupsDir = backupsDir;
|
||||
|
||||
this.profile = profile;
|
||||
this.id = id;
|
||||
|
||||
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.worldInfoTab.setNodeSupplier(() -> new WorldInfoPage(this));
|
||||
this.worldBackupsTab.setNodeSupplier(() -> new WorldBackupsPage(this));
|
||||
this.datapackTab.setNodeSupplier(() -> new DatapackListPage(this));
|
||||
|
||||
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(transitionPane, worldInfoTab, worldBackupsTab);
|
||||
header.select(worldInfoTab);
|
||||
|
||||
// Does it need to be done in the background?
|
||||
try {
|
||||
sessionLockChannel = world.lock();
|
||||
LOG.info("Acquired lock on world " + world.getFileName());
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
|
||||
setCenter(transitionPane);
|
||||
|
||||
BorderPane left = new BorderPane();
|
||||
@@ -106,37 +111,81 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
AdvancedListBox toolbar = new AdvancedListBox();
|
||||
|
||||
if (world.getGameVersion() != null && world.getGameVersion().isAtLeast("1.20", "23w14a")) {
|
||||
toolbar.addNavigationDrawerItem(i18n("version.launch"), SVG.ROCKET_LAUNCH, this::launch, null);
|
||||
toolbar.addNavigationDrawerItem(i18n("version.launch"), SVG.ROCKET_LAUNCH, this::launch, advancedListItem -> advancedListItem.setDisable(isReadOnly()));
|
||||
toolbar.addNavigationDrawerItem(i18n("version.launch_script"), SVG.SCRIPT, this::generateLaunchScript, null);
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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 (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));
|
||||
chunkBasePopupMenu.getContent().add(
|
||||
new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), () -> ChunkBaseApp.openEndCityFinder(world), chunkBasePopup));
|
||||
}
|
||||
|
||||
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(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),
|
||||
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);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (sessionLockChannel == null || !sessionLockChannel.isOpen()) {
|
||||
sessionLockChannel = WorldManageUIUtils.getSessionLockChannel(world);
|
||||
}
|
||||
}
|
||||
|
||||
public void onExited(Navigator.NavigationEvent event) {
|
||||
try {
|
||||
WorldManageUIUtils.closeSessionLockChannel(world, sessionLockChannel);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -156,19 +205,6 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
||||
return sessionLockChannel == null;
|
||||
}
|
||||
|
||||
public void onExited(Navigator.NavigationEvent event) {
|
||||
if (sessionLockChannel != null) {
|
||||
try {
|
||||
sessionLockChannel.close();
|
||||
LOG.info("Releases the lock on world " + world.getFileName());
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to close session lock channel", e);
|
||||
}
|
||||
|
||||
sessionLockChannel = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void launch() {
|
||||
fireEvent(new PageCloseEvent());
|
||||
Versions.launchAndEnterWorld(profile, id, world.getFileName());
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.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.InputDialogPane;
|
||||
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.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() {
|
||||
}
|
||||
|
||||
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))
|
||||
.thenRunAsync(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)));
|
||||
}
|
||||
|
||||
public static void copyWorld(World world, Runnable runnable) {
|
||||
Path worldPath = world.getFile();
|
||||
Controllers.dialog(new InputDialogPane(
|
||||
i18n("world.duplicate.prompt"),
|
||||
"",
|
||||
(result, resolve, reject) -> {
|
||||
if (StringUtils.isBlank(result)) {
|
||||
reject.accept(i18n("world.duplicate.failed.empty_name"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.contains("/") || result.contains("\\") || !FileUtils.isNameValid(result)) {
|
||||
reject.accept(i18n("world.duplicate.failed.invalid_name"));
|
||||
return;
|
||||
}
|
||||
|
||||
Path targetDir = worldPath.resolveSibling(result);
|
||||
if (Files.exists(targetDir)) {
|
||||
reject.accept(i18n("world.duplicate.failed.already_exists"));
|
||||
return;
|
||||
}
|
||||
|
||||
Task.runAsync(Schedulers.io(), () -> world.copy(result))
|
||||
.thenAcceptAsync(Schedulers.javafx(), (Void) -> Controllers.showToast(i18n("world.duplicate.success.toast")))
|
||||
.thenAcceptAsync(Schedulers.javafx(), (Void) -> {
|
||||
if (runnable != null) {
|
||||
runnable.run();
|
||||
}
|
||||
}
|
||||
).whenComplete(Schedulers.javafx(), (throwable) -> {
|
||||
if (throwable == null) {
|
||||
resolve.run();
|
||||
} else {
|
||||
reject.accept(i18n("world.duplicate.failed"));
|
||||
LOG.warning("Failed to duplicate world " + world.getFile(), throwable);
|
||||
}
|
||||
})
|
||||
.start();
|
||||
}));
|
||||
}
|
||||
|
||||
public static void closeSessionLockChannel(World world, FileChannel sessionLockChannel) throws IOException {
|
||||
if (sessionLockChannel != null) {
|
||||
try {
|
||||
sessionLockChannel.close();
|
||||
LOG.info("Closed session lock channel of the world " + world.getFileName());
|
||||
} catch (IOException e) {
|
||||
throw new IOException("Failed to close session lock channel of the world " + world.getFile(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static FileChannel getSessionLockChannel(World world) {
|
||||
try {
|
||||
FileChannel lock = world.lock();
|
||||
LOG.info("Acquired lock on world " + world.getFileName());
|
||||
return lock;
|
||||
} catch (IOException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1131,6 +1131,13 @@ world.chunkbase.end_city=End City
|
||||
world.chunkbase.seed_map=Seed Map
|
||||
world.chunkbase.stronghold=Stronghold
|
||||
world.chunkbase.nether_fortress=Nether Fortress
|
||||
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=Failed to duplicate the world
|
||||
world.duplicate.success.toast=Successfully duplicated the world
|
||||
world.datapack=Datapacks
|
||||
world.datetime=Last played on %s
|
||||
world.delete=Delete the World
|
||||
@@ -1143,6 +1150,15 @@ 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.icon.change=Change world icon
|
||||
world.icon.change.fail.load.title=Failed to parse image
|
||||
world.icon.change.fail.load.text=This image appears to be corrupted, and HMCL cannot parse it.
|
||||
world.icon.change.fail.not_64x64.title=Image size error
|
||||
world.icon.change.fail.not_64x64.text=The image resolution is %d×%d instead of 64×64. Please provide a 64×64 image and try again.
|
||||
world.icon.change.succeed.toast=Successfully updated the world icon.
|
||||
world.icon.change.tip=A 64×64 PNG image is required. Images with an incorrect resolution cannot be parsed by Minecraft.
|
||||
world.icon.choose.title=Select world icon
|
||||
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
|
||||
@@ -1157,6 +1173,7 @@ world.info.difficulty.peaceful=Peaceful
|
||||
world.info.difficulty.easy=Easy
|
||||
world.info.difficulty.normal=Normal
|
||||
world.info.difficulty.hard=Hard
|
||||
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
|
||||
@@ -1177,6 +1194,7 @@ world.info.player.xp_level=Experience Level
|
||||
world.info.random_seed=Seed
|
||||
world.info.time=Game Time
|
||||
world.info.time.format=%s days
|
||||
world.load.fail=Failed to load world
|
||||
world.locked=In use
|
||||
world.locked.failed=The world is currently in use. Please close the game and try again.
|
||||
world.manage=Worlds
|
||||
@@ -1614,4 +1632,4 @@ 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 >
|
||||
@@ -1111,6 +1111,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.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
|
||||
|
||||
@@ -1104,6 +1104,9 @@ world.chunkbase.end_city=Город Края
|
||||
world.chunkbase.seed_map=Предпросмотр генерации мира
|
||||
world.chunkbase.stronghold=Крепость
|
||||
world.chunkbase.nether_fortress=Крепость Нижнего мира
|
||||
world.duplicate.failed.already_exists=Папка уже существует
|
||||
world.duplicate.failed.empty_name=Название не может быть пустым
|
||||
world.duplicate.failed.invalid_name=Название содержит недопустимые символы
|
||||
world.delete=Удалить этот мир
|
||||
world.delete.failed=Не удалось удалить мир.\n%s
|
||||
world.datapack=Наборы данных
|
||||
|
||||
@@ -1048,6 +1048,9 @@ world.chunkbase.end_city=Кінцеве місто
|
||||
world.chunkbase.seed_map=Карта насіння
|
||||
world.chunkbase.stronghold=Фортеця
|
||||
world.chunkbase.nether_fortress=Форт Незеру
|
||||
world.duplicate.failed.already_exists=Каталог вже існує
|
||||
world.duplicate.failed.empty_name=Назва не може бути порожньою
|
||||
world.duplicate.failed.invalid_name=Назва містить недійсні символи
|
||||
world.datapack=Datapacks
|
||||
world.datetime=Останній раз грали %s
|
||||
world.delete=Видалити цей світ
|
||||
|
||||
@@ -919,6 +919,13 @@ world.chunkbase.end_city=終界城地圖
|
||||
world.chunkbase.seed_map=種子地圖
|
||||
world.chunkbase.stronghold=要塞地圖
|
||||
world.chunkbase.nether_fortress=地獄要塞地圖
|
||||
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=刪除此世界
|
||||
@@ -930,6 +937,15 @@ world.export.title=選取該世界的儲存位置
|
||||
world.export.location=儲存到
|
||||
world.export.wizard=匯出世界「%s」
|
||||
world.extension=世界壓縮檔
|
||||
world.icon=世界圖示
|
||||
world.icon.change=修改世界圖示
|
||||
world.icon.change.fail.load.title=圖片解析失敗
|
||||
world.icon.change.fail.load.text=該圖片似乎已損毀,HMCL 無法解析它
|
||||
world.icon.change.fail.not_64x64.title=圖片大小錯誤
|
||||
world.icon.change.fail.not_64x64.text=該圖片的解析度為 %d×%d,而不是 64×64,請提供一張 64×64 解析度的圖片再次嘗試
|
||||
world.icon.change.succeed.toast=世界圖示修改成功
|
||||
world.icon.change.tip=請提供一張 64×64 PNG 格式的圖片。錯誤解析度的圖片將無法被 Minecraft 解析。
|
||||
world.icon.choose.title=選擇世界圖示
|
||||
world.import.already_exists=此世界已經存在
|
||||
world.import.choose=選取要匯入的世界壓縮檔
|
||||
world.import.failed=無法匯入此世界: %s
|
||||
@@ -944,6 +960,7 @@ world.info.difficulty.peaceful=和平
|
||||
world.info.difficulty.easy=簡單
|
||||
world.info.difficulty.normal=普通
|
||||
world.info.difficulty.hard=困難
|
||||
world.info.difficulty_lock=鎖定難易度
|
||||
world.info.failed=讀取世界資訊失敗
|
||||
world.info.game_version=遊戲版本
|
||||
world.info.last_played=上一次遊戲時間
|
||||
@@ -964,6 +981,7 @@ world.info.player.xp_level=經驗等級
|
||||
world.info.random_seed=種子碼
|
||||
world.info.time=遊戲內時間
|
||||
world.info.time.format=%s 天
|
||||
world.load.fail=世界載入失敗
|
||||
world.locked=使用中
|
||||
world.locked.failed=該世界正在使用中,請關閉遊戲後重試。
|
||||
world.game_version=遊戲版本
|
||||
|
||||
@@ -923,6 +923,13 @@ world.chunkbase.end_city=末地城地图
|
||||
world.chunkbase.seed_map=种子地图
|
||||
world.chunkbase.stronghold=要塞地图
|
||||
world.chunkbase.nether_fortress=下界要塞地图
|
||||
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=删除此世界
|
||||
@@ -935,6 +942,15 @@ world.export.location=保存到
|
||||
world.export.wizard=导出世界“%s”
|
||||
world.extension=世界压缩包
|
||||
world.game_version=游戏版本
|
||||
world.icon=世界图标
|
||||
world.icon.change=修改世界图标
|
||||
world.icon.change.fail.load.title=图片解析出错
|
||||
world.icon.change.fail.load.text=该图片似乎已损坏,HMCL 无法解析它
|
||||
world.icon.change.fail.not_64x64.title=图片大小错误
|
||||
world.icon.change.fail.not_64x64.text=该图片的分辨率为 %d×%d,而不是 64×64,请提供一张 64×64 分辨率的图片再次尝试
|
||||
world.icon.change.succeed.toast=世界图标修改成功
|
||||
world.icon.change.tip=请提供一张 64×64 PNG 格式的图片。错误分辨率的图片将无法被 Minecraft 解析。
|
||||
world.icon.choose.title=选择世界图标
|
||||
world.import.already_exists=此世界已经存在
|
||||
world.import.choose=选择要导入的世界压缩包
|
||||
world.import.failed=无法导入此世界:%s
|
||||
@@ -949,6 +965,7 @@ world.info.difficulty.peaceful=和平
|
||||
world.info.difficulty.easy=简单
|
||||
world.info.difficulty.normal=普通
|
||||
world.info.difficulty.hard=困难
|
||||
world.info.difficulty_lock=锁定难度
|
||||
world.info.failed=读取世界信息失败
|
||||
world.info.game_version=游戏版本
|
||||
world.info.last_played=上一次游戏时间
|
||||
@@ -969,6 +986,7 @@ world.info.player.xp_level=经验等级
|
||||
world.info.random_seed=种子
|
||||
world.info.time=游戏内时间
|
||||
world.info.time.format=%s 天
|
||||
world.load.fail=世界加载失败
|
||||
world.locked=使用中
|
||||
world.locked.failed=该世界正在使用中,请关闭游戏后重试。
|
||||
world.manage=世界管理
|
||||
|
||||
@@ -18,10 +18,7 @@
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.github.steveice10.opennbt.NBTIO;
|
||||
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.LongTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.StringTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.Tag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.*;
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.util.io.*;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
@@ -48,13 +45,10 @@ public final class World {
|
||||
|
||||
private final Path file;
|
||||
private String fileName;
|
||||
private String worldName;
|
||||
private GameVersionNumber gameVersion;
|
||||
private long lastPlayed;
|
||||
private CompoundTag levelData;
|
||||
private Image icon;
|
||||
private Long seed;
|
||||
private boolean largeBiomes;
|
||||
private boolean isLocked;
|
||||
private Path levelDataPath;
|
||||
|
||||
public World(Path file) throws IOException {
|
||||
this.file = file;
|
||||
@@ -67,10 +61,100 @@ public final class World {
|
||||
throw new IOException("Path " + file + " cannot be recognized as a Minecraft world");
|
||||
}
|
||||
|
||||
public Path getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public String getWorldName() {
|
||||
CompoundTag data = levelData.get("Data");
|
||||
StringTag levelNameTag = data.get("LevelName");
|
||||
return levelNameTag.getValue();
|
||||
}
|
||||
|
||||
public void setWorldName(String worldName) throws IOException {
|
||||
if (levelData.get("Data") instanceof CompoundTag data && data.get("LevelName") instanceof StringTag levelNameTag) {
|
||||
levelNameTag.setValue(worldName);
|
||||
writeLevelDat(levelData);
|
||||
}
|
||||
}
|
||||
|
||||
public Path getLevelDatFile() {
|
||||
return file.resolve("level.dat");
|
||||
}
|
||||
|
||||
public Path getSessionLockFile() {
|
||||
return file.resolve("session.lock");
|
||||
}
|
||||
|
||||
public CompoundTag getLevelData() {
|
||||
return levelData;
|
||||
}
|
||||
|
||||
public long getLastPlayed() {
|
||||
CompoundTag data = levelData.get("Data");
|
||||
LongTag lastPlayedTag = data.get("LastPlayed");
|
||||
return lastPlayedTag.getValue();
|
||||
}
|
||||
|
||||
public @Nullable GameVersionNumber getGameVersion() {
|
||||
if (levelData.get("Data") instanceof CompoundTag data &&
|
||||
data.get("Version") instanceof CompoundTag versionTag &&
|
||||
versionTag.get("Name") instanceof StringTag nameTag) {
|
||||
return GameVersionNumber.asGameVersion(nameTag.getValue());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public @Nullable Long getSeed() {
|
||||
CompoundTag data = levelData.get("Data");
|
||||
if (data.get("WorldGenSettings") instanceof CompoundTag worldGenSettingsTag && worldGenSettingsTag.get("seed") instanceof LongTag seedTag) { //Valid after 1.16
|
||||
return seedTag.getValue();
|
||||
} else if (data.get("RandomSeed") instanceof LongTag seedTag) { //Valid before 1.16
|
||||
return seedTag.getValue();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean isLargeBiomes() {
|
||||
CompoundTag data = levelData.get("Data");
|
||||
if (data.get("generatorName") instanceof StringTag generatorNameTag) { //Valid before 1.16
|
||||
return "largeBiomes".equals(generatorNameTag.getValue());
|
||||
} else {
|
||||
if (data.get("WorldGenSettings") instanceof CompoundTag worldGenSettingsTag
|
||||
&& worldGenSettingsTag.get("dimensions") instanceof CompoundTag dimensionsTag
|
||||
&& dimensionsTag.get("minecraft:overworld") instanceof CompoundTag overworldTag
|
||||
&& overworldTag.get("generator") instanceof CompoundTag generatorTag) {
|
||||
if (generatorTag.get("biome_source") instanceof CompoundTag biomeSourceTag
|
||||
&& biomeSourceTag.get("large_biomes") instanceof ByteTag largeBiomesTag) { //Valid between 1.16 and 1.16.2
|
||||
return largeBiomesTag.getValue() == (byte) 1;
|
||||
} else if (generatorTag.get("settings") instanceof StringTag settingsTag) { //Valid after 1.16.2
|
||||
return "minecraft:large_biomes".equals(settingsTag.getValue());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public Image getIcon() {
|
||||
return icon;
|
||||
}
|
||||
|
||||
public boolean isLocked() {
|
||||
return isLocked;
|
||||
}
|
||||
|
||||
private void loadFromDirectory() throws IOException {
|
||||
fileName = FileUtils.getName(file);
|
||||
Path levelDat = file.resolve("level.dat");
|
||||
loadWorldInfo(levelDat);
|
||||
if (!Files.exists(levelDat)) { // version 20w14infinite
|
||||
levelDat = file.resolve("special_level.dat");
|
||||
}
|
||||
loadAndCheckLevelDat(levelDat);
|
||||
this.levelDataPath = levelDat;
|
||||
isLocked = isLocked(getSessionLockFile());
|
||||
|
||||
Path iconFile = file.resolve("icon.png");
|
||||
@@ -85,56 +169,16 @@ public final class World {
|
||||
}
|
||||
}
|
||||
|
||||
public Path getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public String getWorldName() {
|
||||
return worldName;
|
||||
}
|
||||
|
||||
public Path getLevelDatFile() {
|
||||
return file.resolve("level.dat");
|
||||
}
|
||||
|
||||
public Path getSessionLockFile() {
|
||||
return file.resolve("session.lock");
|
||||
}
|
||||
|
||||
public long getLastPlayed() {
|
||||
return lastPlayed;
|
||||
}
|
||||
|
||||
public @Nullable GameVersionNumber getGameVersion() {
|
||||
return gameVersion;
|
||||
}
|
||||
|
||||
public @Nullable Long getSeed() {
|
||||
return seed;
|
||||
}
|
||||
|
||||
public boolean isLargeBiomes() {
|
||||
return largeBiomes;
|
||||
}
|
||||
|
||||
public Image getIcon() {
|
||||
return icon;
|
||||
}
|
||||
|
||||
public boolean isLocked() {
|
||||
return isLocked;
|
||||
}
|
||||
|
||||
private void loadFromZipImpl(Path root) throws IOException {
|
||||
Path levelDat = root.resolve("level.dat");
|
||||
if (!Files.exists(levelDat))
|
||||
throw new IOException("Not a valid world zip file since level.dat cannot be found.");
|
||||
if (!Files.exists(levelDat)) { //version 20w14infinite
|
||||
levelDat = root.resolve("special_level.dat");
|
||||
}
|
||||
if (!Files.exists(levelDat)) {
|
||||
throw new IOException("Not a valid world zip file since level.dat or special_level.dat cannot be found.");
|
||||
}
|
||||
|
||||
loadWorldInfo(levelDat);
|
||||
loadAndCheckLevelDat(levelDat);
|
||||
|
||||
Path iconFile = root.resolve("icon.png");
|
||||
if (Files.isRegularFile(iconFile)) {
|
||||
@@ -167,50 +211,22 @@ public final class World {
|
||||
}
|
||||
}
|
||||
|
||||
private void loadWorldInfo(Path levelDat) throws IOException {
|
||||
CompoundTag nbt = parseLevelDat(levelDat);
|
||||
|
||||
CompoundTag data = nbt.get("Data");
|
||||
private void loadAndCheckLevelDat(Path levelDat) throws IOException {
|
||||
this.levelData = parseLevelDat(levelDat);
|
||||
CompoundTag data = levelData.get("Data");
|
||||
if (data == null)
|
||||
throw new IOException("level.dat missing Data");
|
||||
|
||||
if (data.get("LevelName") instanceof StringTag)
|
||||
worldName = data.<StringTag>get("LevelName").getValue();
|
||||
else
|
||||
if (!(data.get("LevelName") instanceof StringTag))
|
||||
throw new IOException("level.dat missing LevelName");
|
||||
|
||||
if (data.get("LastPlayed") instanceof LongTag)
|
||||
lastPlayed = data.<LongTag>get("LastPlayed").getValue();
|
||||
else
|
||||
if (!(data.get("LastPlayed") instanceof LongTag))
|
||||
throw new IOException("level.dat missing LastPlayed");
|
||||
}
|
||||
|
||||
gameVersion = null;
|
||||
if (data.get("Version") instanceof CompoundTag) {
|
||||
CompoundTag version = data.get("Version");
|
||||
|
||||
if (version.get("Name") instanceof StringTag)
|
||||
gameVersion = GameVersionNumber.asGameVersion(version.<StringTag>get("Name").getValue());
|
||||
}
|
||||
|
||||
Tag worldGenSettings = data.get("WorldGenSettings");
|
||||
if (worldGenSettings instanceof CompoundTag) {
|
||||
Tag seedTag = ((CompoundTag) worldGenSettings).get("seed");
|
||||
if (seedTag instanceof LongTag) {
|
||||
seed = ((LongTag) seedTag).getValue();
|
||||
}
|
||||
}
|
||||
if (seed == null) {
|
||||
Tag seedTag = data.get("RandomSeed");
|
||||
if (seedTag instanceof LongTag) {
|
||||
seed = ((LongTag) seedTag).getValue();
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: Only work for 1.15 and below
|
||||
if (data.get("generatorName") instanceof StringTag) {
|
||||
largeBiomes = "largeBiomes".equals(data.<StringTag>get("generatorName").getValue());
|
||||
} else {
|
||||
largeBiomes = false;
|
||||
public void reloadLevelDat() throws IOException {
|
||||
if (levelDataPath != null) {
|
||||
loadAndCheckLevelDat(this.levelDataPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,10 +235,9 @@ public final class World {
|
||||
throw new IOException("Not a valid world directory");
|
||||
|
||||
// Change the name recorded in level.dat
|
||||
CompoundTag nbt = readLevelDat();
|
||||
CompoundTag data = nbt.get("Data");
|
||||
CompoundTag data = levelData.get("Data");
|
||||
data.put(new StringTag("LevelName", newName));
|
||||
writeLevelDat(nbt);
|
||||
writeLevelDat(levelData);
|
||||
|
||||
// then change the folder's name
|
||||
Files.move(file, file.resolveSibling(newName));
|
||||
@@ -283,11 +298,19 @@ public final class World {
|
||||
FileUtils.forceDelete(file);
|
||||
}
|
||||
|
||||
public CompoundTag readLevelDat() throws IOException {
|
||||
if (!Files.isDirectory(file))
|
||||
public void copy(String newName) throws IOException {
|
||||
if (!Files.isDirectory(file)) {
|
||||
throw new IOException("Not a valid world directory");
|
||||
}
|
||||
|
||||
return parseLevelDat(getLevelDatFile());
|
||||
if (isLocked()) {
|
||||
throw new WorldLockedException("The world " + getFile() + " has been locked");
|
||||
}
|
||||
|
||||
Path newPath = file.resolveSibling(newName);
|
||||
FileUtils.copyDirectory(file, newPath, path -> !path.contains("session.lock"));
|
||||
World newWorld = new World(newPath);
|
||||
newWorld.rename(newName);
|
||||
}
|
||||
|
||||
public FileChannel lock() throws WorldLockedException {
|
||||
|
||||
Reference in New Issue
Block a user