feat: 优化世界管理界面和世界信息界面 (#4823)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com>
This commit is contained in:
@@ -18,6 +18,7 @@
|
|||||||
package org.jackhuang.hmcl.ui.versions;
|
package org.jackhuang.hmcl.ui.versions;
|
||||||
|
|
||||||
import com.github.steveice10.opennbt.tag.builtin.*;
|
import com.github.steveice10.opennbt.tag.builtin.*;
|
||||||
|
import com.jfoenix.controls.JFXButton;
|
||||||
import com.jfoenix.controls.JFXComboBox;
|
import com.jfoenix.controls.JFXComboBox;
|
||||||
import com.jfoenix.controls.JFXTextField;
|
import com.jfoenix.controls.JFXTextField;
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
@@ -28,27 +29,36 @@ import javafx.scene.Cursor;
|
|||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.ScrollPane;
|
import javafx.scene.control.ScrollPane;
|
||||||
import javafx.scene.effect.BoxBlur;
|
import javafx.scene.effect.BoxBlur;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.scene.layout.VBox;
|
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.game.World;
|
||||||
import org.jackhuang.hmcl.task.Schedulers;
|
import org.jackhuang.hmcl.task.Schedulers;
|
||||||
import org.jackhuang.hmcl.task.Task;
|
import org.jackhuang.hmcl.task.Task;
|
||||||
|
import org.jackhuang.hmcl.ui.Controllers;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
import org.jackhuang.hmcl.ui.SVG;
|
import org.jackhuang.hmcl.ui.SVG;
|
||||||
import org.jackhuang.hmcl.ui.construct.*;
|
import org.jackhuang.hmcl.ui.construct.*;
|
||||||
|
import org.jetbrains.annotations.PropertyKey;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.text.DecimalFormat;
|
import java.text.DecimalFormat;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Locale;
|
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.formatDateTime;
|
||||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||||
|
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Glavo
|
* @author Glavo
|
||||||
@@ -58,6 +68,8 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
private final World world;
|
private final World world;
|
||||||
private CompoundTag levelDat;
|
private CompoundTag levelDat;
|
||||||
|
|
||||||
|
ImageView iconImageView = new ImageView();
|
||||||
|
|
||||||
public WorldInfoPage(WorldManagePage worldManagePage) {
|
public WorldInfoPage(WorldManagePage worldManagePage) {
|
||||||
this.worldManagePage = worldManagePage;
|
this.worldManagePage = worldManagePage;
|
||||||
this.world = worldManagePage.getWorld();
|
this.world = worldManagePage.getWorld();
|
||||||
@@ -80,7 +92,7 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
if (!Files.isDirectory(world.getFile()))
|
if (!Files.isDirectory(world.getFile()))
|
||||||
throw new IOException("Not a valid world directory");
|
throw new IOException("Not a valid world directory");
|
||||||
|
|
||||||
return world.readLevelDat();
|
return world.getLevelData();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateControls() {
|
private void updateControls() {
|
||||||
@@ -103,98 +115,132 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
{
|
{
|
||||||
BorderPane worldNamePane = new BorderPane();
|
BorderPane worldNamePane = new BorderPane();
|
||||||
{
|
{
|
||||||
Label label = new Label(i18n("world.name"));
|
setLeftLabel(worldNamePane, "world.name");
|
||||||
worldNamePane.setLeft(label);
|
JFXTextField worldNameField = new JFXTextField();
|
||||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
setRightTextField(worldNamePane, worldNameField, 200);
|
||||||
|
|
||||||
Label worldNameLabel = new Label();
|
Tag tag = dataTag.get("LevelName");
|
||||||
FXUtils.copyOnDoubleClick(worldNameLabel);
|
if (tag instanceof StringTag stringTag) {
|
||||||
worldNameLabel.setText(world.getWorldName());
|
worldNameField.setText(stringTag.getValue());
|
||||||
BorderPane.setAlignment(worldNameLabel, Pos.CENTER_RIGHT);
|
|
||||||
worldNamePane.setRight(worldNameLabel);
|
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();
|
BorderPane gameVersionPane = new BorderPane();
|
||||||
{
|
{
|
||||||
Label label = new Label(i18n("world.info.game_version"));
|
setLeftLabel(gameVersionPane, "world.info.game_version");
|
||||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
|
||||||
gameVersionPane.setLeft(label);
|
|
||||||
|
|
||||||
Label gameVersionLabel = new Label();
|
Label gameVersionLabel = new Label();
|
||||||
FXUtils.copyOnDoubleClick(gameVersionLabel);
|
setRightTextLabel(gameVersionPane, gameVersionLabel, () -> world.getGameVersion() == null ? "" : world.getGameVersion().toNormalizedString());
|
||||||
if (world.getGameVersion() != null)
|
|
||||||
gameVersionLabel.setText(world.getGameVersion().toNormalizedString());
|
|
||||||
BorderPane.setAlignment(gameVersionLabel, Pos.CENTER_RIGHT);
|
|
||||||
gameVersionPane.setRight(gameVersionLabel);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BorderPane randomSeedPane = new BorderPane();
|
BorderPane iconPane = new BorderPane();
|
||||||
{
|
{
|
||||||
|
setLeftLabel(iconPane, "world.icon");
|
||||||
|
|
||||||
HBox left = new HBox(8);
|
Runnable onClickAction = () -> Controllers.confirm(
|
||||||
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
i18n("world.icon.change.tip"), i18n("world.icon.change"), MessageDialogPane.MessageType.INFO,
|
||||||
left.setAlignment(Pos.CENTER_LEFT);
|
this::changeWorldIcon,
|
||||||
randomSeedPane.setLeft(left);
|
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();
|
SimpleBooleanProperty visibility = new SimpleBooleanProperty();
|
||||||
StackPane visibilityButton = new StackPane();
|
StackPane visibilityButton = new StackPane();
|
||||||
|
{
|
||||||
visibilityButton.setCursor(Cursor.HAND);
|
visibilityButton.setCursor(Cursor.HAND);
|
||||||
|
visibilityButton.setAlignment(Pos.BOTTOM_RIGHT);
|
||||||
FXUtils.setLimitWidth(visibilityButton, 12);
|
FXUtils.setLimitWidth(visibilityButton, 12);
|
||||||
FXUtils.setLimitHeight(visibilityButton, 12);
|
FXUtils.setLimitHeight(visibilityButton, 12);
|
||||||
FXUtils.onClicked(visibilityButton, () -> visibility.set(!visibility.get()));
|
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Label seedLabel = new Label();
|
||||||
|
{
|
||||||
|
FXUtils.copyOnDoubleClick(seedLabel);
|
||||||
|
seedLabel.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
|
||||||
|
seedLabel.setText(world.getSeed() != null ? world.getSeed().toString() : "");
|
||||||
|
|
||||||
BoxBlur blur = new BoxBlur();
|
BoxBlur blur = new BoxBlur();
|
||||||
blur.setIterations(3);
|
blur.setIterations(3);
|
||||||
FXUtils.onChangeAndOperate(visibility, isVisibility -> {
|
FXUtils.onChangeAndOperate(visibility, isVisibility -> {
|
||||||
SVG icon = isVisibility ? SVG.VISIBILITY : SVG.VISIBILITY_OFF;
|
SVG icon = isVisibility ? SVG.VISIBILITY : SVG.VISIBILITY_OFF;
|
||||||
visibilityButton.getChildren().setAll(icon.createIcon(12));
|
visibilityButton.getChildren().setAll(icon.createIcon(12));
|
||||||
randomSeedLabel.setEffect(isVisibility ? null : blur);
|
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();
|
BorderPane lastPlayedPane = new BorderPane();
|
||||||
{
|
{
|
||||||
Label label = new Label(i18n("world.info.last_played"));
|
setLeftLabel(lastPlayedPane, "world.info.last_played");
|
||||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
|
||||||
lastPlayedPane.setLeft(label);
|
|
||||||
|
|
||||||
Label lastPlayedLabel = new Label();
|
Label lastPlayedLabel = new Label();
|
||||||
FXUtils.copyOnDoubleClick(lastPlayedLabel);
|
setRightTextLabel(lastPlayedPane, lastPlayedLabel, () -> formatDateTime(Instant.ofEpochMilli(world.getLastPlayed())));
|
||||||
lastPlayedLabel.setText(formatDateTime(Instant.ofEpochMilli(world.getLastPlayed())));
|
|
||||||
BorderPane.setAlignment(lastPlayedLabel, Pos.CENTER_RIGHT);
|
|
||||||
lastPlayedPane.setRight(lastPlayedLabel);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BorderPane timePane = new BorderPane();
|
BorderPane timePane = new BorderPane();
|
||||||
{
|
{
|
||||||
Label label = new Label(i18n("world.info.time"));
|
setLeftLabel(timePane, "world.info.time");
|
||||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
|
||||||
timePane.setLeft(label);
|
|
||||||
|
|
||||||
Label timeLabel = new Label();
|
Label timeLabel = new Label();
|
||||||
FXUtils.copyOnDoubleClick(timeLabel);
|
setRightTextLabel(timePane, timeLabel, () -> {
|
||||||
BorderPane.setAlignment(timeLabel, Pos.CENTER_RIGHT);
|
|
||||||
timePane.setRight(timeLabel);
|
|
||||||
|
|
||||||
Tag tag = dataTag.get("Time");
|
Tag tag = dataTag.get("Time");
|
||||||
if (tag instanceof LongTag) {
|
if (tag instanceof LongTag) {
|
||||||
long days = ((LongTag) tag).getValue() / 24000;
|
long days = ((LongTag) tag).getValue() / 24000;
|
||||||
timeLabel.setText(i18n("world.info.time.format", days));
|
return i18n("world.info.time.format", days);
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
OptionToggleButton allowCheatsButton = new OptionToggleButton();
|
OptionToggleButton allowCheatsButton = new OptionToggleButton();
|
||||||
@@ -203,21 +249,7 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
allowCheatsButton.setDisable(worldManagePage.isReadOnly());
|
allowCheatsButton.setDisable(worldManagePage.isReadOnly());
|
||||||
Tag tag = dataTag.get("allowCommands");
|
Tag tag = dataTag.get("allowCommands");
|
||||||
|
|
||||||
if (tag instanceof ByteTag) {
|
checkTagAndSetListener(tag, allowCheatsButton);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
OptionToggleButton generateFeaturesButton = new OptionToggleButton();
|
OptionToggleButton generateFeaturesButton = new OptionToggleButton();
|
||||||
@@ -226,28 +258,12 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
generateFeaturesButton.setDisable(worldManagePage.isReadOnly());
|
generateFeaturesButton.setDisable(worldManagePage.isReadOnly());
|
||||||
Tag tag = worldGenSettings != null ? worldGenSettings.get("generate_features") : dataTag.get("MapFeatures");
|
Tag tag = worldGenSettings != null ? worldGenSettings.get("generate_features") : dataTag.get("MapFeatures");
|
||||||
|
|
||||||
if (tag instanceof ByteTag) {
|
checkTagAndSetListener(tag, generateFeaturesButton);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BorderPane difficultyPane = new BorderPane();
|
BorderPane difficultyPane = new BorderPane();
|
||||||
{
|
{
|
||||||
Label label = new Label(i18n("world.info.difficulty"));
|
setLeftLabel(difficultyPane, "world.info.difficulty");
|
||||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
|
||||||
difficultyPane.setLeft(label);
|
|
||||||
|
|
||||||
JFXComboBox<Difficulty> difficultyBox = new JFXComboBox<>(Difficulty.items);
|
JFXComboBox<Difficulty> difficultyBox = new JFXComboBox<>(Difficulty.items);
|
||||||
difficultyBox.setDisable(worldManagePage.isReadOnly());
|
difficultyBox.setDisable(worldManagePage.isReadOnly());
|
||||||
@@ -255,8 +271,7 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
difficultyPane.setRight(difficultyBox);
|
difficultyPane.setRight(difficultyBox);
|
||||||
|
|
||||||
Tag tag = dataTag.get("Difficulty");
|
Tag tag = dataTag.get("Difficulty");
|
||||||
if (tag instanceof ByteTag) {
|
if (tag instanceof ByteTag byteTag) {
|
||||||
ByteTag byteTag = (ByteTag) tag;
|
|
||||||
Difficulty difficulty = Difficulty.of(byteTag.getValue());
|
Difficulty difficulty = Difficulty.of(byteTag.getValue());
|
||||||
if (difficulty != null) {
|
if (difficulty != null) {
|
||||||
difficultyBox.setValue(difficulty);
|
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(
|
basicInfo.getContent().setAll(
|
||||||
worldNamePane, gameVersionPane, randomSeedPane, lastPlayedPane, timePane,
|
worldNamePane, gameVersionPane, iconPane, seedPane, lastPlayedPane, timePane,
|
||||||
allowCheatsButton, generateFeaturesButton, difficultyPane);
|
allowCheatsButton, generateFeaturesButton, difficultyPane, difficultyLockPane);
|
||||||
|
|
||||||
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info.basic")), basicInfo);
|
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info.basic")), basicInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
Tag playerTag = dataTag.get("Player");
|
Tag playerTag = dataTag.get("Player");
|
||||||
if (playerTag instanceof CompoundTag) {
|
if (playerTag instanceof CompoundTag player) {
|
||||||
CompoundTag player = (CompoundTag) playerTag;
|
|
||||||
ComponentList playerInfo = new ComponentList();
|
ComponentList playerInfo = new ComponentList();
|
||||||
|
|
||||||
BorderPane locationPane = new BorderPane();
|
BorderPane locationPane = new BorderPane();
|
||||||
{
|
{
|
||||||
Label label = new Label(i18n("world.info.player.location"));
|
setLeftLabel(locationPane, "world.info.player.location");
|
||||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
|
||||||
locationPane.setLeft(label);
|
|
||||||
|
|
||||||
Label locationLabel = new Label();
|
Label locationLabel = new Label();
|
||||||
FXUtils.copyOnDoubleClick(locationLabel);
|
setRightTextLabel(locationPane, locationLabel, () -> {
|
||||||
BorderPane.setAlignment(locationLabel, Pos.CENTER_RIGHT);
|
|
||||||
locationPane.setRight(locationLabel);
|
|
||||||
|
|
||||||
Dimension dim = Dimension.of(player.get("Dimension"));
|
Dimension dim = Dimension.of(player.get("Dimension"));
|
||||||
if (dim != null) {
|
if (dim != null) {
|
||||||
String posString = dim.formatPosition(player.get("Pos"));
|
String posString = dim.formatPosition(player.get("Pos"));
|
||||||
if (posString != null)
|
if (posString != null)
|
||||||
locationLabel.setText(posString);
|
return posString;
|
||||||
}
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
BorderPane lastDeathLocationPane = new BorderPane();
|
BorderPane lastDeathLocationPane = new BorderPane();
|
||||||
{
|
{
|
||||||
Label label = new Label(i18n("world.info.player.last_death_location"));
|
setLeftLabel(lastDeathLocationPane, "world.info.player.last_death_location");
|
||||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
|
||||||
lastDeathLocationPane.setLeft(label);
|
|
||||||
|
|
||||||
Label lastDeathLocationLabel = new Label();
|
Label lastDeathLocationLabel = new Label();
|
||||||
FXUtils.copyOnDoubleClick(lastDeathLocationLabel);
|
setRightTextLabel(lastDeathLocationPane, lastDeathLocationLabel, () -> {
|
||||||
BorderPane.setAlignment(lastDeathLocationLabel, Pos.CENTER_RIGHT);
|
Tag tag = player.get("LastDeathLocation");// Valid after 22w14a; prior to this version, the game did not record the last death location data.
|
||||||
lastDeathLocationPane.setRight(lastDeathLocationLabel);
|
if (tag instanceof CompoundTag compoundTag) {
|
||||||
|
Dimension dim = Dimension.of(compoundTag.get("dimension"));
|
||||||
Tag tag = player.get("LastDeathLocation");
|
|
||||||
if (tag instanceof CompoundTag) {
|
|
||||||
Dimension dim = Dimension.of(((CompoundTag) tag).get("dimension"));
|
|
||||||
if (dim != null) {
|
if (dim != null) {
|
||||||
String posString = dim.formatPosition(((CompoundTag) tag).get("pos"));
|
String posString = dim.formatPosition(compoundTag.get("pos"));
|
||||||
if (posString != null)
|
if (posString != null)
|
||||||
lastDeathLocationLabel.setText(posString);
|
return posString;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return "";
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BorderPane spawnPane = new BorderPane();
|
BorderPane spawnPane = new BorderPane();
|
||||||
{
|
{
|
||||||
Label label = new Label(i18n("world.info.player.spawn"));
|
setLeftLabel(spawnPane, "world.info.player.spawn");
|
||||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
|
||||||
spawnPane.setLeft(label);
|
|
||||||
|
|
||||||
Label spawnLabel = new Label();
|
Label spawnLabel = new Label();
|
||||||
FXUtils.copyOnDoubleClick(spawnLabel);
|
setRightTextLabel(spawnPane, spawnLabel, () -> {
|
||||||
BorderPane.setAlignment(spawnLabel, Pos.CENTER_RIGHT);
|
|
||||||
spawnPane.setRight(spawnLabel);
|
|
||||||
|
|
||||||
Dimension dim = Dimension.of(player.get("SpawnDimension"));
|
Dimension dimension;
|
||||||
if (dim != null) {
|
if (player.get("respawn") instanceof CompoundTag respawnTag && respawnTag.get("dimension") != null) { // Valid after 25w07a
|
||||||
Tag x = player.get("SpawnX");
|
dimension = Dimension.of(respawnTag.get("dimension"));
|
||||||
Tag y = player.get("SpawnY");
|
Tag posTag = respawnTag.get("pos");
|
||||||
Tag z = player.get("SpawnZ");
|
|
||||||
|
|
||||||
if (x instanceof IntTag && y instanceof IntTag && z instanceof IntTag)
|
if (posTag instanceof IntArrayTag intArrayTag && intArrayTag.length() >= 3) {
|
||||||
spawnLabel.setText(dim.formatPosition(((IntTag) x).getValue(), ((IntTag) y).getValue(), ((IntTag) z).getValue()));
|
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();
|
BorderPane playerGameTypePane = new BorderPane();
|
||||||
{
|
{
|
||||||
Label label = new Label(i18n("world.info.player.game_type"));
|
setLeftLabel(playerGameTypePane, "world.info.player.game_type");
|
||||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
|
||||||
playerGameTypePane.setLeft(label);
|
|
||||||
|
|
||||||
JFXComboBox<GameType> gameTypeBox = new JFXComboBox<>(GameType.items);
|
JFXComboBox<GameType> gameTypeBox = new JFXComboBox<>(GameType.items);
|
||||||
gameTypeBox.setDisable(worldManagePage.isReadOnly());
|
gameTypeBox.setDisable(worldManagePage.isReadOnly());
|
||||||
@@ -364,8 +386,7 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
Tag hardcoreTag = dataTag.get("hardcore");
|
Tag hardcoreTag = dataTag.get("hardcore");
|
||||||
boolean isHardcore = hardcoreTag instanceof ByteTag && ((ByteTag) hardcoreTag).getValue() == 1;
|
boolean isHardcore = hardcoreTag instanceof ByteTag && ((ByteTag) hardcoreTag).getValue() == 1;
|
||||||
|
|
||||||
if (tag instanceof IntTag) {
|
if (tag instanceof IntTag intTag) {
|
||||||
IntTag intTag = (IntTag) tag;
|
|
||||||
GameType gameType = GameType.of(intTag.getValue(), isHardcore);
|
GameType gameType = GameType.of(intTag.getValue(), isHardcore);
|
||||||
if (gameType != null) {
|
if (gameType != null) {
|
||||||
gameTypeBox.setValue(gameType);
|
gameTypeBox.setValue(gameType);
|
||||||
@@ -395,33 +416,13 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
|
|
||||||
BorderPane healthPane = new BorderPane();
|
BorderPane healthPane = new BorderPane();
|
||||||
{
|
{
|
||||||
Label label = new Label(i18n("world.info.player.health"));
|
setLeftLabel(healthPane, "world.info.player.health");
|
||||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
|
||||||
healthPane.setLeft(label);
|
|
||||||
|
|
||||||
JFXTextField healthField = new JFXTextField();
|
JFXTextField healthField = new JFXTextField();
|
||||||
healthField.setDisable(worldManagePage.isReadOnly());
|
setRightTextField(healthPane, healthField, 50);
|
||||||
healthField.setPrefWidth(50);
|
|
||||||
healthField.setAlignment(Pos.CENTER_RIGHT);
|
|
||||||
BorderPane.setAlignment(healthField, Pos.CENTER_RIGHT);
|
|
||||||
healthPane.setRight(healthField);
|
|
||||||
|
|
||||||
Tag tag = player.get("Health");
|
Tag tag = player.get("Health");
|
||||||
if (tag instanceof FloatTag) {
|
if (tag instanceof FloatTag floatTag) {
|
||||||
FloatTag floatTag = (FloatTag) tag;
|
setTagAndTextField(floatTag, healthField);
|
||||||
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));
|
|
||||||
} else {
|
} else {
|
||||||
healthField.setDisable(true);
|
healthField.setDisable(true);
|
||||||
}
|
}
|
||||||
@@ -429,33 +430,13 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
|
|
||||||
BorderPane foodLevelPane = new BorderPane();
|
BorderPane foodLevelPane = new BorderPane();
|
||||||
{
|
{
|
||||||
Label label = new Label(i18n("world.info.player.food_level"));
|
setLeftLabel(foodLevelPane, "world.info.player.food_level");
|
||||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
|
||||||
foodLevelPane.setLeft(label);
|
|
||||||
|
|
||||||
JFXTextField foodLevelField = new JFXTextField();
|
JFXTextField foodLevelField = new JFXTextField();
|
||||||
foodLevelField.setDisable(worldManagePage.isReadOnly());
|
setRightTextField(foodLevelPane, foodLevelField, 50);
|
||||||
foodLevelField.setPrefWidth(50);
|
|
||||||
foodLevelField.setAlignment(Pos.CENTER_RIGHT);
|
|
||||||
BorderPane.setAlignment(foodLevelField, Pos.CENTER_RIGHT);
|
|
||||||
foodLevelPane.setRight(foodLevelField);
|
|
||||||
|
|
||||||
Tag tag = player.get("foodLevel");
|
Tag tag = player.get("foodLevel");
|
||||||
if (tag instanceof IntTag) {
|
if (tag instanceof IntTag intTag) {
|
||||||
IntTag intTag = (IntTag) tag;
|
setTagAndTextField(intTag, foodLevelField);
|
||||||
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));
|
|
||||||
} else {
|
} else {
|
||||||
foodLevelField.setDisable(true);
|
foodLevelField.setDisable(true);
|
||||||
}
|
}
|
||||||
@@ -463,33 +444,13 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
|
|
||||||
BorderPane xpLevelPane = new BorderPane();
|
BorderPane xpLevelPane = new BorderPane();
|
||||||
{
|
{
|
||||||
Label label = new Label(i18n("world.info.player.xp_level"));
|
setLeftLabel(xpLevelPane, "world.info.player.xp_level");
|
||||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
|
||||||
xpLevelPane.setLeft(label);
|
|
||||||
|
|
||||||
JFXTextField xpLevelField = new JFXTextField();
|
JFXTextField xpLevelField = new JFXTextField();
|
||||||
xpLevelField.setDisable(worldManagePage.isReadOnly());
|
setRightTextField(xpLevelPane, xpLevelField, 50);
|
||||||
xpLevelField.setPrefWidth(50);
|
|
||||||
xpLevelField.setAlignment(Pos.CENTER_RIGHT);
|
|
||||||
BorderPane.setAlignment(xpLevelField, Pos.CENTER_RIGHT);
|
|
||||||
xpLevelPane.setRight(xpLevelField);
|
|
||||||
|
|
||||||
Tag tag = player.get("XpLevel");
|
Tag tag = player.get("XpLevel");
|
||||||
if (tag instanceof IntTag) {
|
if (tag instanceof IntTag intTag) {
|
||||||
IntTag intTag = (IntTag) tag;
|
setTagAndTextField(intTag, xpLevelField);
|
||||||
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));
|
|
||||||
} else {
|
} else {
|
||||||
xpLevelField.setDisable(true);
|
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() {
|
private void saveLevelDat() {
|
||||||
LOG.info("Saving level.dat of world " + world.getWorldName());
|
LOG.info("Saving level.dat of world " + world.getWorldName());
|
||||||
try {
|
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 OVERWORLD = new Dimension(null);
|
||||||
static final Dimension THE_NETHER = new Dimension(i18n("world.info.dimension.the_nether"));
|
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"));
|
static final Dimension THE_END = new Dimension(i18n("world.info.dimension.the_end"));
|
||||||
|
|
||||||
final String name;
|
|
||||||
|
|
||||||
static Dimension of(Tag tag) {
|
static Dimension of(Tag tag) {
|
||||||
if (tag instanceof IntTag) {
|
if (tag instanceof IntTag intTag) {
|
||||||
switch (((IntTag) tag).getValue()) {
|
return switch (intTag.getValue()) {
|
||||||
case 0:
|
case 0 -> OVERWORLD;
|
||||||
return OVERWORLD;
|
case 1 -> THE_NETHER;
|
||||||
case 1:
|
case 2 -> THE_END;
|
||||||
return THE_NETHER;
|
default -> null;
|
||||||
case 2:
|
};
|
||||||
return THE_END;
|
} else if (tag instanceof StringTag stringTag) {
|
||||||
default:
|
String id = stringTag.getValue();
|
||||||
return null;
|
return switch (id) {
|
||||||
}
|
case "overworld", "minecraft:overworld" -> OVERWORLD;
|
||||||
} else if (tag instanceof StringTag) {
|
case "the_nether", "minecraft:the_nether" -> THE_NETHER;
|
||||||
String id = ((StringTag) tag).getValue();
|
case "the_end", "minecraft:the_end" -> THE_END;
|
||||||
switch (id) {
|
default -> new Dimension(id);
|
||||||
case "overworld":
|
};
|
||||||
case "minecraft:overworld":
|
|
||||||
return OVERWORLD;
|
|
||||||
case "the_nether":
|
|
||||||
case "minecraft:the_nether":
|
|
||||||
return THE_NETHER;
|
|
||||||
case "the_end":
|
|
||||||
case "minecraft:the_end":
|
|
||||||
return THE_END;
|
|
||||||
default:
|
|
||||||
return new Dimension(id);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Dimension(String name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
String formatPosition(Tag tag) {
|
String formatPosition(Tag tag) {
|
||||||
if (tag instanceof ListTag) {
|
if (tag instanceof ListTag listTag && listTag.size() == 3) {
|
||||||
ListTag listTag = (ListTag) tag;
|
|
||||||
if (listTag.size() != 3)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
Tag x = listTag.get(0);
|
Tag x = listTag.get(0);
|
||||||
Tag y = listTag.get(1);
|
Tag y = listTag.get(1);
|
||||||
@@ -575,8 +598,7 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag instanceof IntArrayTag) {
|
if (tag instanceof IntArrayTag intArrayTag) {
|
||||||
IntArrayTag intArrayTag = (IntArrayTag) tag;
|
|
||||||
|
|
||||||
int x = intArrayTag.getValue(0);
|
int x = intArrayTag.getValue(0);
|
||||||
int y = intArrayTag.getValue(1);
|
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 final ObservableList<Difficulty> items = FXCollections.observableList(Arrays.asList(values()));
|
||||||
|
|
||||||
static Difficulty of(int d) {
|
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
|
@Override
|
||||||
@@ -633,4 +655,51 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
return i18n("world.info.player.game_type." + name().toLowerCase(Locale.ROOT));
|
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.Control;
|
||||||
import javafx.scene.control.Skin;
|
import javafx.scene.control.Skin;
|
||||||
import javafx.stage.FileChooser;
|
|
||||||
import org.jackhuang.hmcl.game.World;
|
import org.jackhuang.hmcl.game.World;
|
||||||
import org.jackhuang.hmcl.game.WorldLockedException;
|
|
||||||
import org.jackhuang.hmcl.setting.Profile;
|
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.Controllers;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
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 java.nio.file.Path;
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
|
||||||
|
|
||||||
public final class WorldListItem extends Control {
|
public final class WorldListItem extends Control {
|
||||||
private final World world;
|
private final World world;
|
||||||
private final Path backupsDir;
|
private final Path backupsDir;
|
||||||
@@ -61,34 +51,15 @@ public final class WorldListItem extends Control {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void export() {
|
public void export() {
|
||||||
FileChooser fileChooser = new FileChooser();
|
WorldManageUIUtils.export(world);
|
||||||
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)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void delete() {
|
public void delete() {
|
||||||
Controllers.confirm(
|
WorldManageUIUtils.delete(world, () -> parent.remove(this));
|
||||||
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
|
public void copy() {
|
||||||
);
|
WorldManageUIUtils.copyWorld(world, parent::refresh);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reveal() {
|
public void reveal() {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import org.jackhuang.hmcl.util.ChunkBaseApp;
|
|||||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition;
|
import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition;
|
||||||
import static org.jackhuang.hmcl.util.StringUtils.parseColorEscapes;
|
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(
|
popupMenu.getContent().addAll(
|
||||||
new MenuSeparator(),
|
new MenuSeparator(),
|
||||||
new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), item::export, popup),
|
exportMenuItem,
|
||||||
new IconedMenuItem(SVG.DELETE, i18n("world.delete"), item::delete, popup),
|
deleteMenuItem,
|
||||||
new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.world"), item::reveal, popup));
|
duplicateMenuItem
|
||||||
|
);
|
||||||
|
|
||||||
|
popupMenu.getContent().addAll(
|
||||||
|
new MenuSeparator(),
|
||||||
|
new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.world"), item::reveal, popup)
|
||||||
|
);
|
||||||
|
|
||||||
JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup);
|
JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(root, popup);
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
package org.jackhuang.hmcl.ui.versions;
|
package org.jackhuang.hmcl.ui.versions;
|
||||||
|
|
||||||
import com.jfoenix.controls.JFXPopup;
|
import com.jfoenix.controls.JFXPopup;
|
||||||
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
@@ -27,6 +28,7 @@ import javafx.scene.layout.Priority;
|
|||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import org.jackhuang.hmcl.game.World;
|
import org.jackhuang.hmcl.game.World;
|
||||||
import org.jackhuang.hmcl.setting.Profile;
|
import org.jackhuang.hmcl.setting.Profile;
|
||||||
|
import org.jackhuang.hmcl.ui.Controllers;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
import org.jackhuang.hmcl.ui.SVG;
|
import org.jackhuang.hmcl.ui.SVG;
|
||||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
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.DecoratorAnimatedPage;
|
||||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||||
import org.jackhuang.hmcl.util.ChunkBaseApp;
|
import org.jackhuang.hmcl.util.ChunkBaseApp;
|
||||||
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.channels.FileChannel;
|
import java.nio.channels.FileChannel;
|
||||||
@@ -53,6 +56,8 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
|||||||
private final Profile profile;
|
private final Profile profile;
|
||||||
private final String id;
|
private final String id;
|
||||||
|
|
||||||
|
private boolean loadFailed = false;
|
||||||
|
|
||||||
private final TabHeader header;
|
private final TabHeader header;
|
||||||
private final TabHeader.Tab<WorldInfoPage> worldInfoTab = new TabHeader.Tab<>("worldInfoPage");
|
private final TabHeader.Tab<WorldInfoPage> worldInfoTab = new TabHeader.Tab<>("worldInfoPage");
|
||||||
private final TabHeader.Tab<WorldBackupsPage> worldBackupsTab = new TabHeader.Tab<>("worldBackupsPage");
|
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) {
|
public WorldManagePage(World world, Path backupsDir, Profile profile, String id) {
|
||||||
this.world = world;
|
this.world = world;
|
||||||
this.backupsDir = backupsDir;
|
this.backupsDir = backupsDir;
|
||||||
|
|
||||||
this.profile = profile;
|
this.profile = profile;
|
||||||
this.id = id;
|
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.worldInfoTab.setNodeSupplier(() -> new WorldInfoPage(this));
|
||||||
this.worldBackupsTab.setNodeSupplier(() -> new WorldBackupsPage(this));
|
this.worldBackupsTab.setNodeSupplier(() -> new WorldBackupsPage(this));
|
||||||
this.datapackTab.setNodeSupplier(() -> new DatapackListPage(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);
|
this.header = new TabHeader(transitionPane, worldInfoTab, worldBackupsTab);
|
||||||
header.select(worldInfoTab);
|
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);
|
setCenter(transitionPane);
|
||||||
|
|
||||||
BorderPane left = new BorderPane();
|
BorderPane left = new BorderPane();
|
||||||
@@ -106,37 +111,81 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
|||||||
AdvancedListBox toolbar = new AdvancedListBox();
|
AdvancedListBox toolbar = new AdvancedListBox();
|
||||||
|
|
||||||
if (world.getGameVersion() != null && world.getGameVersion().isAtLeast("1.20", "23w14a")) {
|
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);
|
toolbar.addNavigationDrawerItem(i18n("version.launch_script"), SVG.SCRIPT, this::generateLaunchScript, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ChunkBaseApp.isSupported(world)) {
|
if (ChunkBaseApp.isSupported(world)) {
|
||||||
PopupMenu popupMenu = new PopupMenu();
|
PopupMenu chunkBasePopupMenu = new PopupMenu();
|
||||||
JFXPopup popup = new JFXPopup(popupMenu);
|
JFXPopup chunkBasePopup = new JFXPopup(chunkBasePopupMenu);
|
||||||
|
|
||||||
popupMenu.getContent().addAll(
|
|
||||||
new IconedMenuItem(SVG.EXPLORE, i18n("world.chunkbase.seed_map"), () -> ChunkBaseApp.openSeedMap(world), popup),
|
chunkBasePopupMenu.getContent().addAll(
|
||||||
new IconedMenuItem(SVG.VISIBILITY, i18n("world.chunkbase.stronghold"), () -> ChunkBaseApp.openStrongholdFinder(world), popup),
|
new IconedMenuItem(SVG.EXPLORE, i18n("world.chunkbase.seed_map"), () -> ChunkBaseApp.openSeedMap(world), chunkBasePopup),
|
||||||
new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(world), popup)
|
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) {
|
if (world.getGameVersion() != null && world.getGameVersion().compareTo("1.13") >= 0) {
|
||||||
popupMenu.getContent().add(
|
chunkBasePopupMenu.getContent().add(
|
||||||
new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), () -> ChunkBaseApp.openEndCityFinder(world), popup));
|
new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), () -> ChunkBaseApp.openEndCityFinder(world), chunkBasePopup));
|
||||||
}
|
}
|
||||||
|
|
||||||
toolbar.addNavigationDrawerItem(i18n("world.chunkbase"), SVG.EXPLORE, null, chunkBaseMenuItem ->
|
toolbar.addNavigationDrawerItem(i18n("world.chunkbase"), SVG.EXPLORE, null, chunkBaseMenuItem ->
|
||||||
chunkBaseMenuItem.setOnAction(e ->
|
chunkBaseMenuItem.setOnAction(e ->
|
||||||
popup.show(chunkBaseMenuItem,
|
chunkBasePopup.show(chunkBaseMenuItem,
|
||||||
JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT,
|
JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT,
|
||||||
chunkBaseMenuItem.getWidth(), 0)));
|
chunkBaseMenuItem.getWidth(), 0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
toolbar.addNavigationDrawerItem(i18n("settings.game.exploration"), SVG.FOLDER_OPEN, () -> FXUtils.openFolder(world.getFile()), null);
|
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));
|
BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0));
|
||||||
left.setBottom(toolbar);
|
left.setBottom(toolbar);
|
||||||
|
|
||||||
this.addEventHandler(Navigator.NavigationEvent.EXITED, this::onExited);
|
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
|
@Override
|
||||||
@@ -156,19 +205,6 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
|||||||
return sessionLockChannel == null;
|
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() {
|
public void launch() {
|
||||||
fireEvent(new PageCloseEvent());
|
fireEvent(new PageCloseEvent());
|
||||||
Versions.launchAndEnterWorld(profile, id, world.getFileName());
|
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.seed_map=Seed Map
|
||||||
world.chunkbase.stronghold=Stronghold
|
world.chunkbase.stronghold=Stronghold
|
||||||
world.chunkbase.nether_fortress=Nether Fortress
|
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.datapack=Datapacks
|
||||||
world.datetime=Last played on %s
|
world.datetime=Last played on %s
|
||||||
world.delete=Delete the World
|
world.delete=Delete the World
|
||||||
@@ -1143,6 +1150,15 @@ world.export.location=Save As
|
|||||||
world.export.wizard=Export World "%s"
|
world.export.wizard=Export World "%s"
|
||||||
world.extension=World Archive
|
world.extension=World Archive
|
||||||
world.game_version=Game Version
|
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.already_exists=This world already exists.
|
||||||
world.import.choose=Choose world archive you want to import
|
world.import.choose=Choose world archive you want to import
|
||||||
world.import.failed=Failed to import this world: %s
|
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.easy=Easy
|
||||||
world.info.difficulty.normal=Normal
|
world.info.difficulty.normal=Normal
|
||||||
world.info.difficulty.hard=Hard
|
world.info.difficulty.hard=Hard
|
||||||
|
world.info.difficulty_lock=Lock Difficulty
|
||||||
world.info.failed=Failed to read the world info
|
world.info.failed=Failed to read the world info
|
||||||
world.info.game_version=Game Version
|
world.info.game_version=Game Version
|
||||||
world.info.last_played=Last Played
|
world.info.last_played=Last Played
|
||||||
@@ -1177,6 +1194,7 @@ world.info.player.xp_level=Experience Level
|
|||||||
world.info.random_seed=Seed
|
world.info.random_seed=Seed
|
||||||
world.info.time=Game Time
|
world.info.time=Game Time
|
||||||
world.info.time.format=%s days
|
world.info.time.format=%s days
|
||||||
|
world.load.fail=Failed to load world
|
||||||
world.locked=In use
|
world.locked=In use
|
||||||
world.locked.failed=The world is currently in use. Please close the game and try again.
|
world.locked.failed=The world is currently in use. Please close the game and try again.
|
||||||
world.manage=Worlds
|
world.manage=Worlds
|
||||||
|
|||||||
@@ -1111,6 +1111,9 @@ world.chunkbase.end_city=Ciudad del End
|
|||||||
world.chunkbase.seed_map=Vista previa de la generación mundial
|
world.chunkbase.seed_map=Vista previa de la generación mundial
|
||||||
world.chunkbase.stronghold=Fortaleza
|
world.chunkbase.stronghold=Fortaleza
|
||||||
world.chunkbase.nether_fortress=Fortaleza del Nether
|
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.datapack=Paquetes de datos
|
||||||
world.datetime=Jugado por última vez en %s
|
world.datetime=Jugado por última vez en %s
|
||||||
world.delete=Eliminar este mundo
|
world.delete=Eliminar este mundo
|
||||||
|
|||||||
@@ -1104,6 +1104,9 @@ world.chunkbase.end_city=Город Края
|
|||||||
world.chunkbase.seed_map=Предпросмотр генерации мира
|
world.chunkbase.seed_map=Предпросмотр генерации мира
|
||||||
world.chunkbase.stronghold=Крепость
|
world.chunkbase.stronghold=Крепость
|
||||||
world.chunkbase.nether_fortress=Крепость Нижнего мира
|
world.chunkbase.nether_fortress=Крепость Нижнего мира
|
||||||
|
world.duplicate.failed.already_exists=Папка уже существует
|
||||||
|
world.duplicate.failed.empty_name=Название не может быть пустым
|
||||||
|
world.duplicate.failed.invalid_name=Название содержит недопустимые символы
|
||||||
world.delete=Удалить этот мир
|
world.delete=Удалить этот мир
|
||||||
world.delete.failed=Не удалось удалить мир.\n%s
|
world.delete.failed=Не удалось удалить мир.\n%s
|
||||||
world.datapack=Наборы данных
|
world.datapack=Наборы данных
|
||||||
|
|||||||
@@ -1048,6 +1048,9 @@ world.chunkbase.end_city=Кінцеве місто
|
|||||||
world.chunkbase.seed_map=Карта насіння
|
world.chunkbase.seed_map=Карта насіння
|
||||||
world.chunkbase.stronghold=Фортеця
|
world.chunkbase.stronghold=Фортеця
|
||||||
world.chunkbase.nether_fortress=Форт Незеру
|
world.chunkbase.nether_fortress=Форт Незеру
|
||||||
|
world.duplicate.failed.already_exists=Каталог вже існує
|
||||||
|
world.duplicate.failed.empty_name=Назва не може бути порожньою
|
||||||
|
world.duplicate.failed.invalid_name=Назва містить недійсні символи
|
||||||
world.datapack=Datapacks
|
world.datapack=Datapacks
|
||||||
world.datetime=Останній раз грали %s
|
world.datetime=Останній раз грали %s
|
||||||
world.delete=Видалити цей світ
|
world.delete=Видалити цей світ
|
||||||
|
|||||||
@@ -919,6 +919,13 @@ world.chunkbase.end_city=終界城地圖
|
|||||||
world.chunkbase.seed_map=種子地圖
|
world.chunkbase.seed_map=種子地圖
|
||||||
world.chunkbase.stronghold=要塞地圖
|
world.chunkbase.stronghold=要塞地圖
|
||||||
world.chunkbase.nether_fortress=地獄要塞地圖
|
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.datapack=資料包管理
|
||||||
world.datetime=上一次遊戲時間: %s
|
world.datetime=上一次遊戲時間: %s
|
||||||
world.delete=刪除此世界
|
world.delete=刪除此世界
|
||||||
@@ -930,6 +937,15 @@ world.export.title=選取該世界的儲存位置
|
|||||||
world.export.location=儲存到
|
world.export.location=儲存到
|
||||||
world.export.wizard=匯出世界「%s」
|
world.export.wizard=匯出世界「%s」
|
||||||
world.extension=世界壓縮檔
|
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.already_exists=此世界已經存在
|
||||||
world.import.choose=選取要匯入的世界壓縮檔
|
world.import.choose=選取要匯入的世界壓縮檔
|
||||||
world.import.failed=無法匯入此世界: %s
|
world.import.failed=無法匯入此世界: %s
|
||||||
@@ -944,6 +960,7 @@ world.info.difficulty.peaceful=和平
|
|||||||
world.info.difficulty.easy=簡單
|
world.info.difficulty.easy=簡單
|
||||||
world.info.difficulty.normal=普通
|
world.info.difficulty.normal=普通
|
||||||
world.info.difficulty.hard=困難
|
world.info.difficulty.hard=困難
|
||||||
|
world.info.difficulty_lock=鎖定難易度
|
||||||
world.info.failed=讀取世界資訊失敗
|
world.info.failed=讀取世界資訊失敗
|
||||||
world.info.game_version=遊戲版本
|
world.info.game_version=遊戲版本
|
||||||
world.info.last_played=上一次遊戲時間
|
world.info.last_played=上一次遊戲時間
|
||||||
@@ -964,6 +981,7 @@ world.info.player.xp_level=經驗等級
|
|||||||
world.info.random_seed=種子碼
|
world.info.random_seed=種子碼
|
||||||
world.info.time=遊戲內時間
|
world.info.time=遊戲內時間
|
||||||
world.info.time.format=%s 天
|
world.info.time.format=%s 天
|
||||||
|
world.load.fail=世界載入失敗
|
||||||
world.locked=使用中
|
world.locked=使用中
|
||||||
world.locked.failed=該世界正在使用中,請關閉遊戲後重試。
|
world.locked.failed=該世界正在使用中,請關閉遊戲後重試。
|
||||||
world.game_version=遊戲版本
|
world.game_version=遊戲版本
|
||||||
|
|||||||
@@ -923,6 +923,13 @@ world.chunkbase.end_city=末地城地图
|
|||||||
world.chunkbase.seed_map=种子地图
|
world.chunkbase.seed_map=种子地图
|
||||||
world.chunkbase.stronghold=要塞地图
|
world.chunkbase.stronghold=要塞地图
|
||||||
world.chunkbase.nether_fortress=下界要塞地图
|
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.datapack=数据包管理
|
||||||
world.datetime=上一次游戏时间: %s
|
world.datetime=上一次游戏时间: %s
|
||||||
world.delete=删除此世界
|
world.delete=删除此世界
|
||||||
@@ -935,6 +942,15 @@ world.export.location=保存到
|
|||||||
world.export.wizard=导出世界“%s”
|
world.export.wizard=导出世界“%s”
|
||||||
world.extension=世界压缩包
|
world.extension=世界压缩包
|
||||||
world.game_version=游戏版本
|
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.already_exists=此世界已经存在
|
||||||
world.import.choose=选择要导入的世界压缩包
|
world.import.choose=选择要导入的世界压缩包
|
||||||
world.import.failed=无法导入此世界:%s
|
world.import.failed=无法导入此世界:%s
|
||||||
@@ -949,6 +965,7 @@ world.info.difficulty.peaceful=和平
|
|||||||
world.info.difficulty.easy=简单
|
world.info.difficulty.easy=简单
|
||||||
world.info.difficulty.normal=普通
|
world.info.difficulty.normal=普通
|
||||||
world.info.difficulty.hard=困难
|
world.info.difficulty.hard=困难
|
||||||
|
world.info.difficulty_lock=锁定难度
|
||||||
world.info.failed=读取世界信息失败
|
world.info.failed=读取世界信息失败
|
||||||
world.info.game_version=游戏版本
|
world.info.game_version=游戏版本
|
||||||
world.info.last_played=上一次游戏时间
|
world.info.last_played=上一次游戏时间
|
||||||
@@ -969,6 +986,7 @@ world.info.player.xp_level=经验等级
|
|||||||
world.info.random_seed=种子
|
world.info.random_seed=种子
|
||||||
world.info.time=游戏内时间
|
world.info.time=游戏内时间
|
||||||
world.info.time.format=%s 天
|
world.info.time.format=%s 天
|
||||||
|
world.load.fail=世界加载失败
|
||||||
world.locked=使用中
|
world.locked=使用中
|
||||||
world.locked.failed=该世界正在使用中,请关闭游戏后重试。
|
world.locked.failed=该世界正在使用中,请关闭游戏后重试。
|
||||||
world.manage=世界管理
|
world.manage=世界管理
|
||||||
|
|||||||
@@ -18,10 +18,7 @@
|
|||||||
package org.jackhuang.hmcl.game;
|
package org.jackhuang.hmcl.game;
|
||||||
|
|
||||||
import com.github.steveice10.opennbt.NBTIO;
|
import com.github.steveice10.opennbt.NBTIO;
|
||||||
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
import com.github.steveice10.opennbt.tag.builtin.*;
|
||||||
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 javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
import org.jackhuang.hmcl.util.io.*;
|
import org.jackhuang.hmcl.util.io.*;
|
||||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||||
@@ -48,13 +45,10 @@ public final class World {
|
|||||||
|
|
||||||
private final Path file;
|
private final Path file;
|
||||||
private String fileName;
|
private String fileName;
|
||||||
private String worldName;
|
private CompoundTag levelData;
|
||||||
private GameVersionNumber gameVersion;
|
|
||||||
private long lastPlayed;
|
|
||||||
private Image icon;
|
private Image icon;
|
||||||
private Long seed;
|
|
||||||
private boolean largeBiomes;
|
|
||||||
private boolean isLocked;
|
private boolean isLocked;
|
||||||
|
private Path levelDataPath;
|
||||||
|
|
||||||
public World(Path file) throws IOException {
|
public World(Path file) throws IOException {
|
||||||
this.file = file;
|
this.file = file;
|
||||||
@@ -67,10 +61,100 @@ public final class World {
|
|||||||
throw new IOException("Path " + file + " cannot be recognized as a Minecraft 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 {
|
private void loadFromDirectory() throws IOException {
|
||||||
fileName = FileUtils.getName(file);
|
fileName = FileUtils.getName(file);
|
||||||
Path levelDat = file.resolve("level.dat");
|
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());
|
isLocked = isLocked(getSessionLockFile());
|
||||||
|
|
||||||
Path iconFile = file.resolve("icon.png");
|
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 {
|
private void loadFromZipImpl(Path root) throws IOException {
|
||||||
Path levelDat = root.resolve("level.dat");
|
Path levelDat = root.resolve("level.dat");
|
||||||
if (!Files.exists(levelDat))
|
if (!Files.exists(levelDat)) { //version 20w14infinite
|
||||||
throw new IOException("Not a valid world zip file since level.dat cannot be found.");
|
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");
|
Path iconFile = root.resolve("icon.png");
|
||||||
if (Files.isRegularFile(iconFile)) {
|
if (Files.isRegularFile(iconFile)) {
|
||||||
@@ -167,50 +211,22 @@ public final class World {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadWorldInfo(Path levelDat) throws IOException {
|
private void loadAndCheckLevelDat(Path levelDat) throws IOException {
|
||||||
CompoundTag nbt = parseLevelDat(levelDat);
|
this.levelData = parseLevelDat(levelDat);
|
||||||
|
CompoundTag data = levelData.get("Data");
|
||||||
CompoundTag data = nbt.get("Data");
|
|
||||||
if (data == null)
|
if (data == null)
|
||||||
throw new IOException("level.dat missing Data");
|
throw new IOException("level.dat missing Data");
|
||||||
|
|
||||||
if (data.get("LevelName") instanceof StringTag)
|
if (!(data.get("LevelName") instanceof StringTag))
|
||||||
worldName = data.<StringTag>get("LevelName").getValue();
|
|
||||||
else
|
|
||||||
throw new IOException("level.dat missing LevelName");
|
throw new IOException("level.dat missing LevelName");
|
||||||
|
|
||||||
if (data.get("LastPlayed") instanceof LongTag)
|
if (!(data.get("LastPlayed") instanceof LongTag))
|
||||||
lastPlayed = data.<LongTag>get("LastPlayed").getValue();
|
|
||||||
else
|
|
||||||
throw new IOException("level.dat missing LastPlayed");
|
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");
|
public void reloadLevelDat() throws IOException {
|
||||||
if (worldGenSettings instanceof CompoundTag) {
|
if (levelDataPath != null) {
|
||||||
Tag seedTag = ((CompoundTag) worldGenSettings).get("seed");
|
loadAndCheckLevelDat(this.levelDataPath);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,10 +235,9 @@ public final class World {
|
|||||||
throw new IOException("Not a valid world directory");
|
throw new IOException("Not a valid world directory");
|
||||||
|
|
||||||
// Change the name recorded in level.dat
|
// Change the name recorded in level.dat
|
||||||
CompoundTag nbt = readLevelDat();
|
CompoundTag data = levelData.get("Data");
|
||||||
CompoundTag data = nbt.get("Data");
|
|
||||||
data.put(new StringTag("LevelName", newName));
|
data.put(new StringTag("LevelName", newName));
|
||||||
writeLevelDat(nbt);
|
writeLevelDat(levelData);
|
||||||
|
|
||||||
// then change the folder's name
|
// then change the folder's name
|
||||||
Files.move(file, file.resolveSibling(newName));
|
Files.move(file, file.resolveSibling(newName));
|
||||||
@@ -283,11 +298,19 @@ public final class World {
|
|||||||
FileUtils.forceDelete(file);
|
FileUtils.forceDelete(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CompoundTag readLevelDat() throws IOException {
|
public void copy(String newName) throws IOException {
|
||||||
if (!Files.isDirectory(file))
|
if (!Files.isDirectory(file)) {
|
||||||
throw new IOException("Not a valid world directory");
|
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 {
|
public FileChannel lock() throws WorldLockedException {
|
||||||
|
|||||||
Reference in New Issue
Block a user