feat: 优化世界管理与世界信息页面 (#5215)
Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com> Co-authored-by: Glavo <zjx001202@gmail.com>
This commit is contained in:
@@ -17,6 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.ui.versions;
|
package org.jackhuang.hmcl.ui.versions;
|
||||||
|
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.scene.control.Skin;
|
import javafx.scene.control.Skin;
|
||||||
import javafx.stage.FileChooser;
|
import javafx.stage.FileChooser;
|
||||||
@@ -42,20 +43,29 @@ import java.util.regex.Pattern;
|
|||||||
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;
|
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||||
|
|
||||||
public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.DatapackInfoObject> {
|
public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.DatapackInfoObject> implements WorldManagePage.WorldRefreshable {
|
||||||
private final Path worldDir;
|
private final Path worldDir;
|
||||||
private final Datapack datapack;
|
private final Datapack datapack;
|
||||||
|
final BooleanProperty readOnly;
|
||||||
|
|
||||||
public DatapackListPage(WorldManagePage worldManagePage) {
|
public DatapackListPage(WorldManagePage worldManagePage) {
|
||||||
this.worldDir = worldManagePage.getWorld().getFile();
|
this.worldDir = worldManagePage.getWorld().getFile();
|
||||||
datapack = new Datapack(worldDir.resolve("datapacks"));
|
datapack = new Datapack(worldDir.resolve("datapacks"));
|
||||||
setItems(MappedObservableList.create(datapack.getPacks(), DatapackListPageSkin.DatapackInfoObject::new));
|
setItems(MappedObservableList.create(datapack.getPacks(), DatapackListPageSkin.DatapackInfoObject::new));
|
||||||
|
readOnly = worldManagePage.readOnlyProperty();
|
||||||
FXUtils.applyDragListener(this, it -> Objects.equals("zip", FileUtils.getExtension(it)),
|
FXUtils.applyDragListener(this, it -> Objects.equals("zip", FileUtils.getExtension(it)),
|
||||||
mods -> mods.forEach(this::installSingleDatapack), this::refresh);
|
this::installMultiDatapack, this::refresh);
|
||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void installMultiDatapack(List<Path> datapackPath) {
|
||||||
|
datapackPath.forEach(this::installSingleDatapack);
|
||||||
|
if (readOnly.get()) {
|
||||||
|
Controllers.showToast(i18n("datapack.reload.toast"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void installSingleDatapack(Path datapack) {
|
private void installSingleDatapack(Path datapack) {
|
||||||
try {
|
try {
|
||||||
this.datapack.installPack(datapack);
|
this.datapack.installPack(datapack);
|
||||||
@@ -83,7 +93,7 @@ public final class DatapackListPage extends ListPageBase<DatapackListPageSkin.Da
|
|||||||
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
|
List<Path> res = FileUtils.toPaths(chooser.showOpenMultipleDialog(Controllers.getStage()));
|
||||||
|
|
||||||
if (res != null) {
|
if (res != null) {
|
||||||
res.forEach(this::installSingleDatapack);
|
installMultiDatapack(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
datapack.loadFromDir();
|
datapack.loadFromDir();
|
||||||
|
|||||||
@@ -114,16 +114,23 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
|||||||
createToolbarButton2(i18n("search"), SVG.SEARCH, () -> isSearching.set(true))
|
createToolbarButton2(i18n("search"), SVG.SEARCH, () -> isSearching.set(true))
|
||||||
);
|
);
|
||||||
|
|
||||||
selectingToolbar.getChildren().addAll(
|
JFXButton removeButton = createToolbarButton2(i18n("button.remove"), SVG.DELETE, () -> {
|
||||||
createToolbarButton2(i18n("button.remove"), SVG.DELETE, () -> {
|
|
||||||
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
|
Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), () -> {
|
||||||
skinnable.removeSelected(listView.getSelectionModel().getSelectedItems());
|
skinnable.removeSelected(listView.getSelectionModel().getSelectedItems());
|
||||||
}, null);
|
}, null);
|
||||||
}),
|
});
|
||||||
createToolbarButton2(i18n("mods.enable"), SVG.CHECK, () ->
|
JFXButton enableButton = createToolbarButton2(i18n("mods.enable"), SVG.CHECK, () ->
|
||||||
skinnable.enableSelected(listView.getSelectionModel().getSelectedItems())),
|
skinnable.enableSelected(listView.getSelectionModel().getSelectedItems()));
|
||||||
createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () ->
|
JFXButton disableButton = createToolbarButton2(i18n("mods.disable"), SVG.CLOSE, () ->
|
||||||
skinnable.disableSelected(listView.getSelectionModel().getSelectedItems())),
|
skinnable.disableSelected(listView.getSelectionModel().getSelectedItems()));
|
||||||
|
removeButton.disableProperty().bind(getSkinnable().readOnly);
|
||||||
|
enableButton.disableProperty().bind(getSkinnable().readOnly);
|
||||||
|
disableButton.disableProperty().bind(getSkinnable().readOnly);
|
||||||
|
|
||||||
|
selectingToolbar.getChildren().addAll(
|
||||||
|
removeButton,
|
||||||
|
enableButton,
|
||||||
|
disableButton,
|
||||||
createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () ->
|
createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () ->
|
||||||
listView.getSelectionModel().selectRange(0, listView.getItems().size())),//reason for not using selectAll() is that selectAll() first clears all selected then selects all, causing the toolbar to flicker
|
listView.getSelectionModel().selectRange(0, listView.getItems().size())),//reason for not using selectAll() is that selectAll() first clears all selected then selects all, causing the toolbar to flicker
|
||||||
createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () ->
|
createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () ->
|
||||||
@@ -179,7 +186,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
|||||||
center.getStyleClass().add("large-spinner-pane");
|
center.getStyleClass().add("large-spinner-pane");
|
||||||
center.loadingProperty().bind(skinnable.loadingProperty());
|
center.loadingProperty().bind(skinnable.loadingProperty());
|
||||||
|
|
||||||
listView.setCellFactory(x -> new DatapackInfoListCell(listView));
|
listView.setCellFactory(x -> new DatapackInfoListCell(listView, getSkinnable().readOnly));
|
||||||
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||||
this.listView.setItems(filteredList);
|
this.listView.setItems(filteredList);
|
||||||
|
|
||||||
@@ -302,7 +309,7 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
|||||||
final TwoLineListItem content = new TwoLineListItem();
|
final TwoLineListItem content = new TwoLineListItem();
|
||||||
BooleanProperty booleanProperty;
|
BooleanProperty booleanProperty;
|
||||||
|
|
||||||
DatapackInfoListCell(JFXListView<DatapackInfoObject> listView) {
|
DatapackInfoListCell(JFXListView<DatapackInfoObject> listView, BooleanProperty isReadOnlyProperty) {
|
||||||
super(listView);
|
super(listView);
|
||||||
|
|
||||||
HBox container = new HBox(8);
|
HBox container = new HBox(8);
|
||||||
@@ -312,6 +319,8 @@ final class DatapackListPageSkin extends SkinBase<DatapackListPage> {
|
|||||||
content.setMouseTransparent(true);
|
content.setMouseTransparent(true);
|
||||||
setSelectable();
|
setSelectable();
|
||||||
|
|
||||||
|
checkBox.disableProperty().bind(isReadOnlyProperty);
|
||||||
|
|
||||||
imageView.setFitWidth(32);
|
imageView.setFitWidth(32);
|
||||||
imageView.setFitHeight(32);
|
imageView.setFitHeight(32);
|
||||||
imageView.setPreserveRatio(true);
|
imageView.setPreserveRatio(true);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
package org.jackhuang.hmcl.ui.versions;
|
package org.jackhuang.hmcl.ui.versions;
|
||||||
|
|
||||||
import com.jfoenix.controls.JFXButton;
|
import com.jfoenix.controls.JFXButton;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
@@ -61,18 +62,18 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
|||||||
/**
|
/**
|
||||||
* @author Glavo
|
* @author Glavo
|
||||||
*/
|
*/
|
||||||
public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.BackupInfo> {
|
public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.BackupInfo> implements WorldManagePage.WorldRefreshable {
|
||||||
static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss");
|
static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss");
|
||||||
|
|
||||||
private final World world;
|
private final World world;
|
||||||
private final Path backupsDir;
|
private final Path backupsDir;
|
||||||
private final boolean isReadOnly;
|
private final BooleanProperty readOnly;
|
||||||
private final Pattern backupFileNamePattern;
|
private final Pattern backupFileNamePattern;
|
||||||
|
|
||||||
public WorldBackupsPage(WorldManagePage worldManagePage) {
|
public WorldBackupsPage(WorldManagePage worldManagePage) {
|
||||||
this.world = worldManagePage.getWorld();
|
this.world = worldManagePage.getWorld();
|
||||||
this.backupsDir = worldManagePage.getBackupsDir();
|
this.backupsDir = worldManagePage.getBackupsDir();
|
||||||
this.isReadOnly = worldManagePage.isReadOnly();
|
this.readOnly = worldManagePage.readOnlyProperty();
|
||||||
this.backupFileNamePattern = Pattern.compile("(?<datetime>[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2})_" + Pattern.quote(world.getFileName()) + "( (?<count>[0-9]+))?\\.zip");
|
this.backupFileNamePattern = Pattern.compile("(?<datetime>[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}-[0-9]{2}-[0-9]{2})_" + Pattern.quote(world.getFileName()) + "( (?<count>[0-9]+))?\\.zip");
|
||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
@@ -164,7 +165,7 @@ public final class WorldBackupsPage extends ListPageBase<WorldBackupsPage.Backup
|
|||||||
@Override
|
@Override
|
||||||
protected List<Node> initializeToolbar(WorldBackupsPage skinnable) {
|
protected List<Node> initializeToolbar(WorldBackupsPage skinnable) {
|
||||||
JFXButton createBackup = createToolbarButton2(i18n("world.backup.create.new_one"), SVG.ARCHIVE, skinnable::createBackup);
|
JFXButton createBackup = createToolbarButton2(i18n("world.backup.create.new_one"), SVG.ARCHIVE, skinnable::createBackup);
|
||||||
createBackup.setDisable(isReadOnly);
|
createBackup.disableProperty().bind(getSkinnable().readOnly);
|
||||||
|
|
||||||
return Arrays.asList(
|
return Arrays.asList(
|
||||||
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh),
|
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh),
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import com.github.steveice10.opennbt.tag.builtin.*;
|
|||||||
import com.jfoenix.controls.JFXButton;
|
import com.jfoenix.controls.JFXButton;
|
||||||
import com.jfoenix.controls.JFXTextField;
|
import com.jfoenix.controls.JFXTextField;
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
@@ -35,7 +36,6 @@ 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 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;
|
||||||
@@ -43,13 +43,17 @@ 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.jackhuang.hmcl.util.Lang;
|
||||||
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
|
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||||
|
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||||
import org.jetbrains.annotations.PropertyKey;
|
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.nio.file.Path;
|
||||||
import java.text.DecimalFormat;
|
import java.text.DecimalFormat;
|
||||||
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@@ -62,8 +66,9 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
|||||||
/**
|
/**
|
||||||
* @author Glavo
|
* @author Glavo
|
||||||
*/
|
*/
|
||||||
public final class WorldInfoPage extends SpinnerPane {
|
public final class WorldInfoPage extends SpinnerPane implements WorldManagePage.WorldRefreshable {
|
||||||
private final WorldManagePage worldManagePage;
|
private final WorldManagePage worldManagePage;
|
||||||
|
private boolean isReadOnly;
|
||||||
private final World world;
|
private final World world;
|
||||||
private CompoundTag levelDat;
|
private CompoundTag levelDat;
|
||||||
|
|
||||||
@@ -72,19 +77,7 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
public WorldInfoPage(WorldManagePage worldManagePage) {
|
public WorldInfoPage(WorldManagePage worldManagePage) {
|
||||||
this.worldManagePage = worldManagePage;
|
this.worldManagePage = worldManagePage;
|
||||||
this.world = worldManagePage.getWorld();
|
this.world = worldManagePage.getWorld();
|
||||||
|
refresh();
|
||||||
this.setLoading(true);
|
|
||||||
Task.supplyAsync(this::loadWorldInfo)
|
|
||||||
.whenComplete(Schedulers.javafx(), ((result, exception) -> {
|
|
||||||
if (exception == null) {
|
|
||||||
this.levelDat = result;
|
|
||||||
updateControls();
|
|
||||||
setLoading(false);
|
|
||||||
} else {
|
|
||||||
LOG.warning("Failed to load level.dat", exception);
|
|
||||||
setFailedReason(i18n("world.info.failed"));
|
|
||||||
}
|
|
||||||
})).start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private CompoundTag loadWorldInfo() throws IOException {
|
private CompoundTag loadWorldInfo() throws IOException {
|
||||||
@@ -96,7 +89,6 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
|
|
||||||
private void updateControls() {
|
private void updateControls() {
|
||||||
CompoundTag dataTag = levelDat.get("Data");
|
CompoundTag dataTag = levelDat.get("Data");
|
||||||
CompoundTag worldGenSettings = dataTag.get("WorldGenSettings");
|
|
||||||
|
|
||||||
ScrollPane scrollPane = new ScrollPane();
|
ScrollPane scrollPane = new ScrollPane();
|
||||||
scrollPane.setFitToHeight(true);
|
scrollPane.setFitToHeight(true);
|
||||||
@@ -110,7 +102,7 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
FXUtils.smoothScrolling(scrollPane);
|
FXUtils.smoothScrolling(scrollPane);
|
||||||
rootPane.getStyleClass().add("card-list");
|
rootPane.getStyleClass().add("card-list");
|
||||||
|
|
||||||
ComponentList basicInfo = new ComponentList();
|
ComponentList worldInfo = new ComponentList();
|
||||||
{
|
{
|
||||||
BorderPane worldNamePane = new BorderPane();
|
BorderPane worldNamePane = new BorderPane();
|
||||||
{
|
{
|
||||||
@@ -118,14 +110,15 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
JFXTextField worldNameField = new JFXTextField();
|
JFXTextField worldNameField = new JFXTextField();
|
||||||
setRightTextField(worldNamePane, worldNameField, 200);
|
setRightTextField(worldNamePane, worldNameField, 200);
|
||||||
|
|
||||||
Tag tag = dataTag.get("LevelName");
|
if (dataTag.get("LevelName") instanceof StringTag worldNameTag) {
|
||||||
if (tag instanceof StringTag stringTag) {
|
var worldName = new SimpleStringProperty(worldNameTag.getValue());
|
||||||
worldNameField.setText(stringTag.getValue());
|
FXUtils.bindString(worldNameField, worldName);
|
||||||
|
worldNameField.getProperties().put(WorldInfoPage.class.getName() + ".worldNameProperty", worldName);
|
||||||
worldNameField.textProperty().addListener((observable, oldValue, newValue) -> {
|
worldName.addListener((observable, oldValue, newValue) -> {
|
||||||
if (newValue != null) {
|
if (StringUtils.isNotBlank(newValue)) {
|
||||||
try {
|
try {
|
||||||
world.setWorldName(newValue);
|
world.setWorldName(newValue);
|
||||||
|
worldManagePage.setTitle(i18n("world.manage.title", StringUtils.parseColorEscapes(world.getWorldName())));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOG.warning("Failed to set world name", e);
|
LOG.warning("Failed to set world name", e);
|
||||||
}
|
}
|
||||||
@@ -139,22 +132,15 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
BorderPane gameVersionPane = new BorderPane();
|
BorderPane gameVersionPane = new BorderPane();
|
||||||
{
|
{
|
||||||
setLeftLabel(gameVersionPane, "world.info.game_version");
|
setLeftLabel(gameVersionPane, "world.info.game_version");
|
||||||
Label gameVersionLabel = new Label();
|
setRightTextLabel(gameVersionPane, () -> world.getGameVersion() == null ? "" : world.getGameVersion().toNormalizedString());
|
||||||
setRightTextLabel(gameVersionPane, gameVersionLabel, () -> world.getGameVersion() == null ? "" : world.getGameVersion().toNormalizedString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BorderPane iconPane = new BorderPane();
|
BorderPane iconPane = new BorderPane();
|
||||||
{
|
{
|
||||||
setLeftLabel(iconPane, "world.icon");
|
setLeftLabel(iconPane, "world.icon");
|
||||||
|
|
||||||
Runnable onClickAction = () -> Controllers.confirm(
|
|
||||||
i18n("world.icon.change.tip"), i18n("world.icon.change"), MessageDialogPane.MessageType.INFO,
|
|
||||||
this::changeWorldIcon,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
FXUtils.limitSize(iconImageView, 32, 32);
|
|
||||||
{
|
{
|
||||||
|
FXUtils.limitSize(iconImageView, 32, 32);
|
||||||
iconImageView.setImage(world.getIcon() == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : world.getIcon());
|
iconImageView.setImage(world.getIcon() == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : world.getIcon());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,14 +148,20 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
JFXButton resetIconButton = new JFXButton();
|
JFXButton resetIconButton = new JFXButton();
|
||||||
{
|
{
|
||||||
editIconButton.setGraphic(SVG.EDIT.createIcon(20));
|
editIconButton.setGraphic(SVG.EDIT.createIcon(20));
|
||||||
editIconButton.setDisable(worldManagePage.isReadOnly());
|
editIconButton.setDisable(isReadOnly);
|
||||||
FXUtils.onClicked(editIconButton, onClickAction);
|
editIconButton.setOnAction(event -> Controllers.confirm(
|
||||||
|
I18n.i18n("world.icon.change.tip"),
|
||||||
|
I18n.i18n("world.icon.change"),
|
||||||
|
MessageDialogPane.MessageType.INFO,
|
||||||
|
this::changeWorldIcon,
|
||||||
|
null
|
||||||
|
));
|
||||||
FXUtils.installFastTooltip(editIconButton, i18n("button.edit"));
|
FXUtils.installFastTooltip(editIconButton, i18n("button.edit"));
|
||||||
editIconButton.getStyleClass().add("toggle-icon4");
|
editIconButton.getStyleClass().add("toggle-icon4");
|
||||||
|
|
||||||
resetIconButton.setGraphic(SVG.RESTORE.createIcon(20));
|
resetIconButton.setGraphic(SVG.RESTORE.createIcon(20));
|
||||||
resetIconButton.setDisable(worldManagePage.isReadOnly());
|
resetIconButton.setDisable(isReadOnly);
|
||||||
FXUtils.onClicked(resetIconButton, this::clearWorldIcon);
|
resetIconButton.setOnAction(event -> this.clearWorldIcon());
|
||||||
FXUtils.installFastTooltip(resetIconButton, i18n("button.reset"));
|
FXUtils.installFastTooltip(resetIconButton, i18n("button.reset"));
|
||||||
resetIconButton.getStyleClass().add("toggle-icon4");
|
resetIconButton.getStyleClass().add("toggle-icon4");
|
||||||
}
|
}
|
||||||
@@ -189,9 +181,7 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
StackPane visibilityButton = new StackPane();
|
StackPane visibilityButton = new StackPane();
|
||||||
{
|
{
|
||||||
visibilityButton.setCursor(Cursor.HAND);
|
visibilityButton.setCursor(Cursor.HAND);
|
||||||
visibilityButton.setAlignment(Pos.BOTTOM_RIGHT);
|
visibilityButton.setAlignment(Pos.CENTER_RIGHT);
|
||||||
FXUtils.setLimitWidth(visibilityButton, 12);
|
|
||||||
FXUtils.setLimitHeight(visibilityButton, 12);
|
|
||||||
FXUtils.onClicked(visibilityButton, () -> visibility.set(!visibility.get()));
|
FXUtils.onClicked(visibilityButton, () -> visibility.set(!visibility.get()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,23 +209,38 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BorderPane worldSpawnPoint = new BorderPane();
|
||||||
|
{
|
||||||
|
setLeftLabel(worldSpawnPoint, "world.info.spawn");
|
||||||
|
setRightTextLabel(worldSpawnPoint, () -> {
|
||||||
|
if (dataTag.get("spawn") instanceof CompoundTag spawnTag && spawnTag.get("pos") instanceof IntArrayTag posTag) {
|
||||||
|
return Dimension.of(spawnTag.get("dimension") instanceof StringTag dimensionTag
|
||||||
|
? dimensionTag
|
||||||
|
: new StringTag("SpawnDimension", "minecraft:overworld"))
|
||||||
|
.formatPosition(posTag);
|
||||||
|
} else if (dataTag.get("SpawnX") instanceof IntTag intX
|
||||||
|
&& dataTag.get("SpawnY") instanceof IntTag intY
|
||||||
|
&& dataTag.get("SpawnZ") instanceof IntTag intZ) {
|
||||||
|
return Dimension.OVERWORLD.formatPosition(intX.getValue(), intY.getValue(), intZ.getValue());
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
BorderPane lastPlayedPane = new BorderPane();
|
BorderPane lastPlayedPane = new BorderPane();
|
||||||
{
|
{
|
||||||
setLeftLabel(lastPlayedPane, "world.info.last_played");
|
setLeftLabel(lastPlayedPane, "world.info.last_played");
|
||||||
Label lastPlayedLabel = new Label();
|
setRightTextLabel(lastPlayedPane, () -> formatDateTime(Instant.ofEpochMilli(world.getLastPlayed())));
|
||||||
setRightTextLabel(lastPlayedPane, lastPlayedLabel, () -> formatDateTime(Instant.ofEpochMilli(world.getLastPlayed())));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BorderPane timePane = new BorderPane();
|
BorderPane timePane = new BorderPane();
|
||||||
{
|
{
|
||||||
setLeftLabel(timePane, "world.info.time");
|
setLeftLabel(timePane, "world.info.time");
|
||||||
|
setRightTextLabel(timePane, () -> {
|
||||||
Label timeLabel = new Label();
|
if (dataTag.get("Time") instanceof LongTag timeTag) {
|
||||||
setRightTextLabel(timePane, timeLabel, () -> {
|
Duration duration = Duration.ofSeconds(timeTag.getValue() / 20);
|
||||||
Tag tag = dataTag.get("Time");
|
return i18n("world.info.time.format", duration.toDays(), duration.toHoursPart(), duration.toMinutesPart());
|
||||||
if (tag instanceof LongTag) {
|
|
||||||
long days = ((LongTag) tag).getValue() / 24000;
|
|
||||||
return i18n("world.info.time.format", days);
|
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -245,19 +250,22 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
LineToggleButton allowCheatsButton = new LineToggleButton();
|
LineToggleButton allowCheatsButton = new LineToggleButton();
|
||||||
{
|
{
|
||||||
allowCheatsButton.setTitle(i18n("world.info.allow_cheats"));
|
allowCheatsButton.setTitle(i18n("world.info.allow_cheats"));
|
||||||
allowCheatsButton.setDisable(worldManagePage.isReadOnly());
|
allowCheatsButton.setDisable(isReadOnly);
|
||||||
Tag tag = dataTag.get("allowCommands");
|
|
||||||
|
|
||||||
checkTagAndSetListener(tag, allowCheatsButton);
|
bindTagAndToggleButton(dataTag.get("allowCommands"), allowCheatsButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
LineToggleButton generateFeaturesButton = new LineToggleButton();
|
LineToggleButton generateFeaturesButton = new LineToggleButton();
|
||||||
{
|
{
|
||||||
generateFeaturesButton.setTitle(i18n("world.info.generate_features"));
|
generateFeaturesButton.setTitle(i18n("world.info.generate_features"));
|
||||||
generateFeaturesButton.setDisable(worldManagePage.isReadOnly());
|
generateFeaturesButton.setDisable(isReadOnly);
|
||||||
Tag tag = worldGenSettings != null ? worldGenSettings.get("generate_features") : dataTag.get("MapFeatures");
|
|
||||||
|
|
||||||
checkTagAndSetListener(tag, generateFeaturesButton);
|
// generate_features was valid after 20w20a and MapFeatures was before that
|
||||||
|
if (dataTag.get("WorldGenSettings") instanceof CompoundTag worldGenSettings) {
|
||||||
|
bindTagAndToggleButton(worldGenSettings.get("generate_features"), generateFeaturesButton);
|
||||||
|
} else {
|
||||||
|
bindTagAndToggleButton(dataTag.get("MapFeatures"), generateFeaturesButton);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LineSelectButton<Difficulty> difficultyButton = new LineSelectButton<>();
|
LineSelectButton<Difficulty> difficultyButton = new LineSelectButton<>();
|
||||||
@@ -266,14 +274,13 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
difficultyButton.setDisable(worldManagePage.isReadOnly());
|
difficultyButton.setDisable(worldManagePage.isReadOnly());
|
||||||
difficultyButton.setItems(Difficulty.items);
|
difficultyButton.setItems(Difficulty.items);
|
||||||
|
|
||||||
Tag tag = dataTag.get("Difficulty");
|
if (dataTag.get("Difficulty") instanceof ByteTag difficultyTag) {
|
||||||
if (tag instanceof ByteTag byteTag) {
|
Difficulty difficulty = Difficulty.of(difficultyTag.getValue());
|
||||||
Difficulty difficulty = Difficulty.of(byteTag.getValue());
|
|
||||||
if (difficulty != null) {
|
if (difficulty != null) {
|
||||||
difficultyButton.setValue(difficulty);
|
difficultyButton.setValue(difficulty);
|
||||||
difficultyButton.valueProperty().addListener((o, oldValue, newValue) -> {
|
difficultyButton.valueProperty().addListener((o, oldValue, newValue) -> {
|
||||||
if (newValue != null) {
|
if (newValue != null) {
|
||||||
byteTag.setValue((byte) newValue.ordinal());
|
difficultyTag.setValue((byte) newValue.ordinal());
|
||||||
saveLevelDat();
|
saveLevelDat();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -288,33 +295,28 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
LineToggleButton difficultyLockPane = new LineToggleButton();
|
LineToggleButton difficultyLockPane = new LineToggleButton();
|
||||||
{
|
{
|
||||||
difficultyLockPane.setTitle(i18n("world.info.difficulty_lock"));
|
difficultyLockPane.setTitle(i18n("world.info.difficulty_lock"));
|
||||||
difficultyLockPane.setDisable(worldManagePage.isReadOnly());
|
difficultyLockPane.setDisable(isReadOnly);
|
||||||
|
|
||||||
Tag tag = dataTag.get("DifficultyLocked");
|
bindTagAndToggleButton(dataTag.get("DifficultyLocked"), difficultyLockPane);
|
||||||
checkTagAndSetListener(tag, difficultyLockPane);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
basicInfo.getContent().setAll(
|
worldInfo.getContent().setAll(
|
||||||
worldNamePane, gameVersionPane, iconPane, seedPane, lastPlayedPane, timePane,
|
worldNamePane, gameVersionPane, iconPane, seedPane, worldSpawnPoint, lastPlayedPane, timePane,
|
||||||
allowCheatsButton, generateFeaturesButton, difficultyButton, difficultyLockPane);
|
allowCheatsButton, generateFeaturesButton, difficultyButton, difficultyLockPane);
|
||||||
|
|
||||||
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info.basic")), basicInfo);
|
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info")), worldInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
Tag playerTag = dataTag.get("Player");
|
if (dataTag.get("Player") instanceof CompoundTag playerTag) {
|
||||||
if (playerTag instanceof CompoundTag player) {
|
|
||||||
ComponentList playerInfo = new ComponentList();
|
ComponentList playerInfo = new ComponentList();
|
||||||
|
|
||||||
BorderPane locationPane = new BorderPane();
|
BorderPane locationPane = new BorderPane();
|
||||||
{
|
{
|
||||||
setLeftLabel(locationPane, "world.info.player.location");
|
setLeftLabel(locationPane, "world.info.player.location");
|
||||||
Label locationLabel = new Label();
|
setRightTextLabel(locationPane, () -> {
|
||||||
setRightTextLabel(locationPane, locationLabel, () -> {
|
Dimension dimension = Dimension.of(playerTag.get("Dimension"));
|
||||||
Dimension dim = Dimension.of(player.get("Dimension"));
|
if (dimension != null && playerTag.get("Pos") instanceof ListTag posTag) {
|
||||||
if (dim != null) {
|
return dimension.formatPosition(posTag);
|
||||||
String posString = dim.formatPosition(player.get("Pos"));
|
|
||||||
if (posString != null)
|
|
||||||
return posString;
|
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
});
|
});
|
||||||
@@ -323,15 +325,12 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
BorderPane lastDeathLocationPane = new BorderPane();
|
BorderPane lastDeathLocationPane = new BorderPane();
|
||||||
{
|
{
|
||||||
setLeftLabel(lastDeathLocationPane, "world.info.player.last_death_location");
|
setLeftLabel(lastDeathLocationPane, "world.info.player.last_death_location");
|
||||||
Label lastDeathLocationLabel = new Label();
|
setRightTextLabel(lastDeathLocationPane, () -> {
|
||||||
setRightTextLabel(lastDeathLocationPane, lastDeathLocationLabel, () -> {
|
// Valid after 22w14a; prior to this version, the game did not record the last death location data.
|
||||||
Tag tag = player.get("LastDeathLocation");// Valid after 22w14a; prior to this version, the game did not record the last death location data.
|
if (playerTag.get("LastDeathLocation") instanceof CompoundTag LastDeathLocationTag) {
|
||||||
if (tag instanceof CompoundTag compoundTag) {
|
Dimension dimension = Dimension.of(LastDeathLocationTag.get("dimension"));
|
||||||
Dimension dim = Dimension.of(compoundTag.get("dimension"));
|
if (dimension != null && LastDeathLocationTag.get("pos") instanceof IntArrayTag posTag) {
|
||||||
if (dim != null) {
|
return dimension.formatPosition(posTag);
|
||||||
String posString = dim.formatPosition(compoundTag.get("pos"));
|
|
||||||
if (posString != null)
|
|
||||||
return posString;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
@@ -342,116 +341,84 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
BorderPane spawnPane = new BorderPane();
|
BorderPane spawnPane = new BorderPane();
|
||||||
{
|
{
|
||||||
setLeftLabel(spawnPane, "world.info.player.spawn");
|
setLeftLabel(spawnPane, "world.info.player.spawn");
|
||||||
Label spawnLabel = new Label();
|
setRightTextLabel(spawnPane, () -> {
|
||||||
setRightTextLabel(spawnPane, spawnLabel, () -> {
|
|
||||||
|
|
||||||
Dimension dimension;
|
if (playerTag.get("respawn") instanceof CompoundTag respawnTag
|
||||||
if (player.get("respawn") instanceof CompoundTag respawnTag && respawnTag.get("dimension") != null) { // Valid after 25w07a
|
&& respawnTag.get("dimension") instanceof StringTag dimensionTag
|
||||||
dimension = Dimension.of(respawnTag.get("dimension"));
|
&& respawnTag.get("pos") instanceof IntArrayTag intArrayTag
|
||||||
Tag posTag = respawnTag.get("pos");
|
&& intArrayTag.length() >= 3) { // Valid after 25w07a
|
||||||
|
return Dimension.of(dimensionTag).formatPosition(intArrayTag);
|
||||||
if (posTag instanceof IntArrayTag intArrayTag && intArrayTag.length() >= 3) {
|
} else if (playerTag.get("SpawnX") instanceof IntTag intX
|
||||||
return dimension.formatPosition(intArrayTag.getValue(0), intArrayTag.getValue(1), intArrayTag.getValue(2));
|
&& playerTag.get("SpawnY") instanceof IntTag intY
|
||||||
}
|
&& playerTag.get("SpawnZ") instanceof IntTag intZ) { // Valid before 25w07a
|
||||||
} 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.
|
// 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"));
|
return (playerTag.get("SpawnDimension") instanceof StringTag dimensionTag ? Dimension.of(dimensionTag) : Dimension.OVERWORLD)
|
||||||
if (dimension == null) {
|
.formatPosition(intX.getValue(), intY.getValue(), intZ.getValue());
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return dimension.formatPosition(intX.getValue(), intY.getValue(), intZ.getValue());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
LineSelectButton<GameType> playerGameTypeButton = new LineSelectButton<>();
|
LineSelectButton<GameType> playerGameTypePane = new LineSelectButton<>();
|
||||||
{
|
{
|
||||||
playerGameTypeButton.setTitle(i18n("world.info.player.game_type"));
|
playerGameTypePane.setTitle(i18n("world.info.player.game_type"));
|
||||||
playerGameTypeButton.setDisable(worldManagePage.isReadOnly());
|
playerGameTypePane.setDisable(worldManagePage.isReadOnly());
|
||||||
playerGameTypeButton.setItems(GameType.items);
|
playerGameTypePane.setItems(GameType.items);
|
||||||
|
|
||||||
Tag tag = player.get("playerGameType");
|
if (playerTag.get("playerGameType") instanceof IntTag playerGameTypeTag
|
||||||
Tag hardcoreTag = dataTag.get("hardcore");
|
&& dataTag.get("hardcore") instanceof ByteTag hardcoreTag) {
|
||||||
boolean isHardcore = hardcoreTag instanceof ByteTag && ((ByteTag) hardcoreTag).getValue() == 1;
|
boolean isHardcore = hardcoreTag.getValue() == 1;
|
||||||
|
GameType gameType = GameType.of(playerGameTypeTag.getValue(), isHardcore);
|
||||||
if (tag instanceof IntTag intTag) {
|
|
||||||
GameType gameType = GameType.of(intTag.getValue(), isHardcore);
|
|
||||||
if (gameType != null) {
|
if (gameType != null) {
|
||||||
playerGameTypeButton.setValue(gameType);
|
playerGameTypePane.setValue(gameType);
|
||||||
playerGameTypeButton.valueProperty().addListener((o, oldValue, newValue) -> {
|
playerGameTypePane.valueProperty().addListener((o, oldValue, newValue) -> {
|
||||||
if (newValue != null) {
|
if (newValue != null) {
|
||||||
if (newValue == GameType.HARDCORE) {
|
if (newValue == GameType.HARDCORE) {
|
||||||
intTag.setValue(0); // survival (hardcore worlds are survival+hardcore flag)
|
playerGameTypeTag.setValue(0); // survival (hardcore worlds are survival+hardcore flag)
|
||||||
if (hardcoreTag instanceof ByteTag) {
|
hardcoreTag.setValue((byte) 1);
|
||||||
((ByteTag) hardcoreTag).setValue((byte) 1);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
intTag.setValue(newValue.ordinal());
|
playerGameTypeTag.setValue(newValue.ordinal());
|
||||||
if (hardcoreTag instanceof ByteTag) {
|
hardcoreTag.setValue((byte) 0);
|
||||||
((ByteTag) hardcoreTag).setValue((byte) 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
saveLevelDat();
|
saveLevelDat();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
playerGameTypeButton.setDisable(true);
|
playerGameTypePane.setDisable(true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
playerGameTypeButton.setDisable(true);
|
playerGameTypePane.setDisable(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BorderPane healthPane = new BorderPane();
|
BorderPane healthPane = new BorderPane();
|
||||||
{
|
{
|
||||||
setLeftLabel(healthPane, "world.info.player.health");
|
setLeftLabel(healthPane, "world.info.player.health");
|
||||||
JFXTextField healthField = new JFXTextField();
|
setRightTextField(healthPane, 50, playerTag.get("Health"));
|
||||||
setRightTextField(healthPane, healthField, 50);
|
|
||||||
|
|
||||||
Tag tag = player.get("Health");
|
|
||||||
if (tag instanceof FloatTag floatTag) {
|
|
||||||
setTagAndTextField(floatTag, healthField);
|
|
||||||
} else {
|
|
||||||
healthField.setDisable(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
BorderPane foodLevelPane = new BorderPane();
|
BorderPane foodLevelPane = new BorderPane();
|
||||||
{
|
{
|
||||||
setLeftLabel(foodLevelPane, "world.info.player.food_level");
|
setLeftLabel(foodLevelPane, "world.info.player.food_level");
|
||||||
JFXTextField foodLevelField = new JFXTextField();
|
setRightTextField(foodLevelPane, 50, playerTag.get("foodLevel"));
|
||||||
setRightTextField(foodLevelPane, foodLevelField, 50);
|
|
||||||
|
|
||||||
Tag tag = player.get("foodLevel");
|
|
||||||
if (tag instanceof IntTag intTag) {
|
|
||||||
setTagAndTextField(intTag, foodLevelField);
|
|
||||||
} else {
|
|
||||||
foodLevelField.setDisable(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BorderPane foodSaturationPane = new BorderPane();
|
||||||
|
{
|
||||||
|
setLeftLabel(foodSaturationPane, "world.info.player.food_saturation_level");
|
||||||
|
setRightTextField(foodSaturationPane, 50, playerTag.get("foodSaturationLevel"));
|
||||||
}
|
}
|
||||||
|
|
||||||
BorderPane xpLevelPane = new BorderPane();
|
BorderPane xpLevelPane = new BorderPane();
|
||||||
{
|
{
|
||||||
setLeftLabel(xpLevelPane, "world.info.player.xp_level");
|
setLeftLabel(xpLevelPane, "world.info.player.xp_level");
|
||||||
JFXTextField xpLevelField = new JFXTextField();
|
setRightTextField(xpLevelPane, 50, playerTag.get("XpLevel"));
|
||||||
setRightTextField(xpLevelPane, xpLevelField, 50);
|
|
||||||
|
|
||||||
Tag tag = player.get("XpLevel");
|
|
||||||
if (tag instanceof IntTag intTag) {
|
|
||||||
setTagAndTextField(intTag, xpLevelField);
|
|
||||||
} else {
|
|
||||||
xpLevelField.setDisable(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
playerInfo.getContent().setAll(
|
playerInfo.getContent().setAll(
|
||||||
locationPane, lastDeathLocationPane, spawnPane,
|
locationPane, lastDeathLocationPane, spawnPane, playerGameTypePane,
|
||||||
playerGameTypeButton, healthPane, foodLevelPane, xpLevelPane
|
healthPane, foodLevelPane, foodSaturationPane, xpLevelPane
|
||||||
);
|
);
|
||||||
|
|
||||||
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info.player")), playerInfo);
|
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info.player")), playerInfo);
|
||||||
@@ -464,14 +431,27 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
borderPane.setLeft(label);
|
borderPane.setLeft(label);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setRightTextField(BorderPane borderPane, int perfWidth, Tag tag) {
|
||||||
|
JFXTextField textField = new JFXTextField();
|
||||||
|
setRightTextField(borderPane, textField, perfWidth);
|
||||||
|
if (tag instanceof IntTag intTag) {
|
||||||
|
bindTagAndTextField(intTag, textField);
|
||||||
|
} else if (tag instanceof FloatTag floatTag) {
|
||||||
|
bindTagAndTextField(floatTag, textField);
|
||||||
|
} else {
|
||||||
|
textField.setDisable(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void setRightTextField(BorderPane borderPane, JFXTextField textField, int perfWidth) {
|
private void setRightTextField(BorderPane borderPane, JFXTextField textField, int perfWidth) {
|
||||||
textField.setDisable(worldManagePage.isReadOnly());
|
textField.setDisable(isReadOnly);
|
||||||
textField.setPrefWidth(perfWidth);
|
textField.setPrefWidth(perfWidth);
|
||||||
textField.setAlignment(Pos.CENTER_RIGHT);
|
BorderPane.setAlignment(textField, Pos.CENTER_RIGHT);
|
||||||
borderPane.setRight(textField);
|
borderPane.setRight(textField);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setRightTextLabel(BorderPane borderPane, Label label, Callable<String> setNameCall) {
|
private void setRightTextLabel(BorderPane borderPane, Callable<String> setNameCall) {
|
||||||
|
Label label = new Label();
|
||||||
FXUtils.copyOnDoubleClick(label);
|
FXUtils.copyOnDoubleClick(label);
|
||||||
BorderPane.setAlignment(label, Pos.CENTER_RIGHT);
|
BorderPane.setAlignment(label, Pos.CENTER_RIGHT);
|
||||||
try {
|
try {
|
||||||
@@ -482,7 +462,7 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
borderPane.setRight(label);
|
borderPane.setRight(label);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkTagAndSetListener(Tag tag, LineToggleButton toggleButton) {
|
private void bindTagAndToggleButton(Tag tag, LineToggleButton toggleButton) {
|
||||||
if (tag instanceof ByteTag byteTag) {
|
if (tag instanceof ByteTag byteTag) {
|
||||||
byte value = byteTag.getValue();
|
byte value = byteTag.getValue();
|
||||||
if (value == 0 || value == 1) {
|
if (value == 0 || value == 1) {
|
||||||
@@ -504,14 +484,17 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setTagAndTextField(IntTag intTag, JFXTextField jfxTextField) {
|
private void bindTagAndTextField(IntTag intTag, JFXTextField jfxTextField) {
|
||||||
jfxTextField.setText(String.valueOf(intTag.getValue()));
|
jfxTextField.setText(intTag.getValue().toString());
|
||||||
|
|
||||||
jfxTextField.textProperty().addListener((o, oldValue, newValue) -> {
|
jfxTextField.textProperty().addListener((o, oldValue, newValue) -> {
|
||||||
if (newValue != null) {
|
if (newValue != null) {
|
||||||
try {
|
try {
|
||||||
intTag.setValue(Integer.parseInt(newValue));
|
Integer integer = Lang.toIntOrNull(newValue);
|
||||||
|
if (integer != null) {
|
||||||
|
intTag.setValue(integer);
|
||||||
saveLevelDat();
|
saveLevelDat();
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
jfxTextField.setText(oldValue);
|
jfxTextField.setText(oldValue);
|
||||||
LOG.warning("Exception happened when saving level.dat", e);
|
LOG.warning("Exception happened when saving level.dat", e);
|
||||||
@@ -522,14 +505,17 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
jfxTextField.setValidators(new NumberValidator(i18n("input.number"), true));
|
jfxTextField.setValidators(new NumberValidator(i18n("input.number"), true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setTagAndTextField(FloatTag floatTag, JFXTextField jfxTextField) {
|
private void bindTagAndTextField(FloatTag floatTag, JFXTextField jfxTextField) {
|
||||||
jfxTextField.setText(new DecimalFormat("#").format(floatTag.getValue().floatValue()));
|
jfxTextField.setText(new DecimalFormat("0.#").format(floatTag.getValue()));
|
||||||
|
|
||||||
jfxTextField.textProperty().addListener((o, oldValue, newValue) -> {
|
jfxTextField.textProperty().addListener((o, oldValue, newValue) -> {
|
||||||
if (newValue != null) {
|
if (newValue != null) {
|
||||||
try {
|
try {
|
||||||
floatTag.setValue(Float.parseFloat(newValue));
|
Float floatValue = Lang.toFloatOrNull(newValue);
|
||||||
|
if (floatValue != null) {
|
||||||
|
floatTag.setValue(floatValue);
|
||||||
saveLevelDat();
|
saveLevelDat();
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
jfxTextField.setText(oldValue);
|
jfxTextField.setText(oldValue);
|
||||||
LOG.warning("Exception happened when saving level.dat", e);
|
LOG.warning("Exception happened when saving level.dat", e);
|
||||||
@@ -549,6 +535,23 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void refresh() {
|
||||||
|
this.isReadOnly = worldManagePage.isReadOnly();
|
||||||
|
this.setLoading(true);
|
||||||
|
Task.supplyAsync(this::loadWorldInfo)
|
||||||
|
.whenComplete(Schedulers.javafx(), ((result, exception) -> {
|
||||||
|
if (exception == null) {
|
||||||
|
this.levelDat = result;
|
||||||
|
updateControls();
|
||||||
|
setLoading(false);
|
||||||
|
} else {
|
||||||
|
LOG.warning("Failed to load level.dat", exception);
|
||||||
|
setFailedReason(i18n("world.info.failed"));
|
||||||
|
}
|
||||||
|
})).start();
|
||||||
|
}
|
||||||
|
|
||||||
private record Dimension(String name) {
|
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"));
|
||||||
@@ -558,8 +561,8 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
if (tag instanceof IntTag intTag) {
|
if (tag instanceof IntTag intTag) {
|
||||||
return switch (intTag.getValue()) {
|
return switch (intTag.getValue()) {
|
||||||
case 0 -> OVERWORLD;
|
case 0 -> OVERWORLD;
|
||||||
case 1 -> THE_NETHER;
|
case -1 -> THE_NETHER;
|
||||||
case 2 -> THE_END;
|
case 1 -> THE_END;
|
||||||
default -> null;
|
default -> null;
|
||||||
};
|
};
|
||||||
} else if (tag instanceof StringTag stringTag) {
|
} else if (tag instanceof StringTag stringTag) {
|
||||||
@@ -655,12 +658,12 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.png"), "*.png"));
|
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.png"), "*.png"));
|
||||||
fileChooser.setInitialFileName("icon.png");
|
fileChooser.setInitialFileName("icon.png");
|
||||||
|
|
||||||
File file = fileChooser.showOpenDialog(Controllers.getStage());
|
Path iconPath = FileUtils.toPath(fileChooser.showOpenDialog(Controllers.getStage()));
|
||||||
if (file == null) return;
|
if (iconPath == null) return;
|
||||||
|
|
||||||
Image image;
|
Image image;
|
||||||
try {
|
try {
|
||||||
image = FXUtils.loadImage(file.toPath());
|
image = FXUtils.loadImage(iconPath);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOG.warning("Failed to load image", 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);
|
Controllers.dialog(i18n("world.icon.change.fail.load.text"), i18n("world.icon.change.fail.load.title"), MessageDialogPane.MessageType.ERROR);
|
||||||
@@ -668,16 +671,16 @@ public final class WorldInfoPage extends SpinnerPane {
|
|||||||
}
|
}
|
||||||
if ((int) image.getWidth() == 64 && (int) image.getHeight() == 64) {
|
if ((int) image.getWidth() == 64 && (int) image.getHeight() == 64) {
|
||||||
Path output = world.getFile().resolve("icon.png");
|
Path output = world.getFile().resolve("icon.png");
|
||||||
saveImage(image, output);
|
saveWorldIcon(iconPath, image, output);
|
||||||
} else {
|
} 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);
|
Controllers.dialog(i18n("world.icon.change.fail.not_64x64.text", (int) image.getWidth(), (int) image.getHeight()), i18n("world.icon.change.fail.not_64x64.title"), MessageDialogPane.MessageType.ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveImage(Image image, Path path) {
|
private void saveWorldIcon(Path sourcePath, Image image, Path targetPath) {
|
||||||
Image oldImage = iconImageView.getImage();
|
Image oldImage = iconImageView.getImage();
|
||||||
try {
|
try {
|
||||||
PNGJavaFXUtils.writeImage(image, path);
|
FileUtils.copyFile(sourcePath, targetPath);
|
||||||
iconImageView.setImage(image);
|
iconImageView.setImage(image);
|
||||||
Controllers.showToast(i18n("world.icon.change.succeed.toast"));
|
Controllers.showToast(i18n("world.icon.change.succeed.toast"));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ import java.nio.file.Path;
|
|||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
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;
|
||||||
@@ -65,10 +64,9 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
|||||||
private final BooleanProperty showAll = new SimpleBooleanProperty(this, "showAll", false);
|
private final BooleanProperty showAll = new SimpleBooleanProperty(this, "showAll", false);
|
||||||
|
|
||||||
private Path savesDir;
|
private Path savesDir;
|
||||||
private Path backupsDir;
|
|
||||||
private List<World> worlds;
|
private List<World> worlds;
|
||||||
private Profile profile;
|
private Profile profile;
|
||||||
private String id;
|
private String instanceId;
|
||||||
|
|
||||||
private int refreshCount = 0;
|
private int refreshCount = 0;
|
||||||
|
|
||||||
@@ -88,9 +86,8 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
|||||||
@Override
|
@Override
|
||||||
public void loadVersion(Profile profile, String id) {
|
public void loadVersion(Profile profile, String id) {
|
||||||
this.profile = profile;
|
this.profile = profile;
|
||||||
this.id = id;
|
this.instanceId = id;
|
||||||
this.savesDir = profile.getRepository().getSavesDirectory(id);
|
this.savesDir = profile.getRepository().getSavesDirectory(id);
|
||||||
this.backupsDir = profile.getRepository().getBackupsDirectory(id);
|
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +97,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
|||||||
} else if (showAll.get()) {
|
} else if (showAll.get()) {
|
||||||
getItems().setAll(worlds);
|
getItems().setAll(worlds);
|
||||||
} else {
|
} else {
|
||||||
GameVersionNumber gameVersion = profile.getRepository().getGameVersion(id).map(GameVersionNumber::asGameVersion).orElse(null);
|
GameVersionNumber gameVersion = profile.getRepository().getGameVersion(instanceId).map(GameVersionNumber::asGameVersion).orElse(null);
|
||||||
getItems().setAll(worlds.stream()
|
getItems().setAll(worlds.stream()
|
||||||
.filter(world -> world.getGameVersion() == null || world.getGameVersion().equals(gameVersion))
|
.filter(world -> world.getGameVersion() == null || world.getGameVersion().equals(gameVersion))
|
||||||
.toList());
|
.toList());
|
||||||
@@ -108,7 +105,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void refresh() {
|
public void refresh() {
|
||||||
if (profile == null || id == null)
|
if (profile == null || instanceId == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
int currentRefresh = ++refreshCount;
|
int currentRefresh = ++refreshCount;
|
||||||
@@ -116,10 +113,8 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
Task.supplyAsync(Schedulers.io(), () -> {
|
Task.supplyAsync(Schedulers.io(), () -> {
|
||||||
// Ensure the game version number is parsed
|
// Ensure the game version number is parsed
|
||||||
profile.getRepository().getGameVersion(id);
|
profile.getRepository().getGameVersion(instanceId);
|
||||||
try (Stream<World> stream = World.getWorlds(savesDir)) {
|
return World.getWorlds(savesDir);
|
||||||
return stream.toList();
|
|
||||||
}
|
|
||||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||||
if (refreshCount != currentRefresh) {
|
if (refreshCount != currentRefresh) {
|
||||||
// A newer refresh task is running, discard this result
|
// A newer refresh task is running, discard this result
|
||||||
@@ -177,7 +172,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void showManagePage(World world) {
|
private void showManagePage(World world) {
|
||||||
Controllers.navigate(new WorldManagePage(world, backupsDir, profile, id));
|
Controllers.navigate(new WorldManagePage(world, profile, instanceId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void export(World world) {
|
public void export(World world) {
|
||||||
@@ -197,11 +192,11 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void launch(World world) {
|
public void launch(World world) {
|
||||||
Versions.launchAndEnterWorld(profile, id, world.getFileName());
|
Versions.launchAndEnterWorld(profile, instanceId, world.getFileName());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void generateLaunchScript(World world) {
|
public void generateLaunchScript(World world) {
|
||||||
Versions.generateLaunchScriptForQuickEnterWorld(profile, id, world.getFileName());
|
Versions.generateLaunchScriptForQuickEnterWorld(profile, instanceId, world.getFileName());
|
||||||
}
|
}
|
||||||
|
|
||||||
public BooleanProperty showAllProperty() {
|
public BooleanProperty showAllProperty() {
|
||||||
@@ -216,14 +211,15 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<Node> initializeToolbar(WorldListPage skinnable) {
|
protected List<Node> initializeToolbar(WorldListPage skinnable) {
|
||||||
JFXCheckBox chkShowAll = new JFXCheckBox();
|
JFXCheckBox chkShowAll = new JFXCheckBox(i18n("world.show_all"));
|
||||||
chkShowAll.setText(i18n("world.show_all"));
|
|
||||||
chkShowAll.selectedProperty().bindBidirectional(skinnable.showAllProperty());
|
chkShowAll.selectedProperty().bindBidirectional(skinnable.showAllProperty());
|
||||||
|
|
||||||
return Arrays.asList(chkShowAll,
|
return Arrays.asList(
|
||||||
|
chkShowAll,
|
||||||
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh),
|
createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh),
|
||||||
createToolbarButton2(i18n("world.add"), SVG.ADD, skinnable::add),
|
createToolbarButton2(i18n("world.add"), SVG.ADD, skinnable::add),
|
||||||
createToolbarButton2(i18n("world.download"), SVG.DOWNLOAD, skinnable::download));
|
createToolbarButton2(i18n("world.download"), SVG.DOWNLOAD, skinnable::download)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -240,6 +236,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
|||||||
private final ImageView imageView;
|
private final ImageView imageView;
|
||||||
private final Tooltip leftTooltip;
|
private final Tooltip leftTooltip;
|
||||||
private final TwoLineListItem content;
|
private final TwoLineListItem content;
|
||||||
|
private final JFXButton btnLaunch;
|
||||||
|
|
||||||
public WorldListCell(WorldListPage page) {
|
public WorldListCell(WorldListPage page) {
|
||||||
this.page = page;
|
this.page = page;
|
||||||
@@ -271,6 +268,17 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
|||||||
root.setRight(right);
|
root.setRight(right);
|
||||||
right.setAlignment(Pos.CENTER_RIGHT);
|
right.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
|
||||||
|
btnLaunch = new JFXButton();
|
||||||
|
right.getChildren().add(btnLaunch);
|
||||||
|
btnLaunch.getStyleClass().add("toggle-icon4");
|
||||||
|
btnLaunch.setGraphic(SVG.ROCKET_LAUNCH.createIcon());
|
||||||
|
FXUtils.installFastTooltip(btnLaunch, i18n("version.launch"));
|
||||||
|
btnLaunch.setOnAction(event -> {
|
||||||
|
World world = getItem();
|
||||||
|
if (world != null)
|
||||||
|
page.launch(world);
|
||||||
|
});
|
||||||
|
|
||||||
JFXButton btnMore = new JFXButton();
|
JFXButton btnMore = new JFXButton();
|
||||||
right.getChildren().add(btnMore);
|
right.getChildren().add(btnMore);
|
||||||
btnMore.getStyleClass().add("toggle-icon4");
|
btnMore.getStyleClass().add("toggle-icon4");
|
||||||
@@ -317,8 +325,12 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
|||||||
|
|
||||||
if (world.getGameVersion() != null)
|
if (world.getGameVersion() != null)
|
||||||
content.addTag(I18n.getDisplayVersion(world.getGameVersion()));
|
content.addTag(I18n.getDisplayVersion(world.getGameVersion()));
|
||||||
if (world.isLocked())
|
if (world.isLocked()) {
|
||||||
content.addTag(i18n("world.locked"));
|
content.addTag(i18n("world.locked"));
|
||||||
|
btnLaunch.setDisable(true);
|
||||||
|
} else {
|
||||||
|
btnLaunch.setDisable(false);
|
||||||
|
}
|
||||||
|
|
||||||
content.setSubtitle(i18n("world.datetime", formatDateTime(Instant.ofEpochMilli(world.getLastPlayed()))));
|
content.setSubtitle(i18n("world.datetime", formatDateTime(Instant.ofEpochMilli(world.getLastPlayed()))));
|
||||||
|
|
||||||
@@ -329,13 +341,15 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
|||||||
// Popup Menu
|
// Popup Menu
|
||||||
|
|
||||||
public void showPopupMenu(World world, JFXPopup.PopupHPosition hPosition, double initOffsetX, double initOffsetY) {
|
public void showPopupMenu(World world, JFXPopup.PopupHPosition hPosition, double initOffsetX, double initOffsetY) {
|
||||||
|
boolean worldLocked = world.isLocked();
|
||||||
|
|
||||||
PopupMenu popupMenu = new PopupMenu();
|
PopupMenu popupMenu = new PopupMenu();
|
||||||
JFXPopup popup = new JFXPopup(popupMenu);
|
JFXPopup popup = new JFXPopup(popupMenu);
|
||||||
|
|
||||||
if (world.getGameVersion() != null && world.getGameVersion().isAtLeast("1.20", "23w14a")) {
|
if (world.supportQuickPlay()) {
|
||||||
|
|
||||||
IconedMenuItem launchItem = new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch_and_enter_world"), () -> page.launch(world), popup);
|
IconedMenuItem launchItem = new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch_and_enter_world"), () -> page.launch(world), popup);
|
||||||
launchItem.setDisable(world.isLocked());
|
launchItem.setDisable(worldLocked);
|
||||||
popupMenu.getContent().add(launchItem);
|
popupMenu.getContent().add(launchItem);
|
||||||
|
|
||||||
popupMenu.getContent().addAll(
|
popupMenu.getContent().addAll(
|
||||||
@@ -354,18 +368,19 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
|
|||||||
new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(world), popup)
|
new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(world), popup)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (world.getGameVersion() != null && world.getGameVersion().compareTo("1.13") >= 0) {
|
if (ChunkBaseApp.supportEndCity(world)) {
|
||||||
popupMenu.getContent().add(new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"),
|
popupMenu.getContent().add(new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), () -> ChunkBaseApp.openEndCityFinder(world), popup));
|
||||||
() -> ChunkBaseApp.openEndCityFinder(world), popup));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IconedMenuItem exportMenuItem = new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> page.export(world), popup);
|
IconedMenuItem exportMenuItem = new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> page.export(world), popup);
|
||||||
|
exportMenuItem.setDisable(worldLocked);
|
||||||
|
|
||||||
IconedMenuItem deleteMenuItem = new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> page.delete(world), popup);
|
IconedMenuItem deleteMenuItem = new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> page.delete(world), popup);
|
||||||
|
deleteMenuItem.setDisable(worldLocked);
|
||||||
|
|
||||||
IconedMenuItem duplicateMenuItem = new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), () -> page.copy(world), popup);
|
IconedMenuItem duplicateMenuItem = new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), () -> page.copy(world), popup);
|
||||||
boolean worldLocked = world.isLocked();
|
duplicateMenuItem.setDisable(worldLocked);
|
||||||
Stream.of(exportMenuItem, deleteMenuItem, duplicateMenuItem)
|
|
||||||
.forEach(iconedMenuItem -> iconedMenuItem.setDisable(worldLocked));
|
|
||||||
|
|
||||||
popupMenu.getContent().addAll(
|
popupMenu.getContent().addAll(
|
||||||
new MenuSeparator(),
|
new MenuSeparator(),
|
||||||
|
|||||||
@@ -19,9 +19,7 @@ package org.jackhuang.hmcl.ui.versions;
|
|||||||
|
|
||||||
import com.jfoenix.controls.JFXPopup;
|
import com.jfoenix.controls.JFXPopup;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.*;
|
||||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
@@ -37,6 +35,7 @@ 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 org.jackhuang.hmcl.util.StringUtils;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.channels.FileChannel;
|
import java.nio.channels.FileChannel;
|
||||||
@@ -50,143 +49,92 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
|||||||
*/
|
*/
|
||||||
public final class WorldManagePage extends DecoratorAnimatedPage implements DecoratorPage {
|
public final class WorldManagePage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||||
|
|
||||||
private final ObjectProperty<State> state;
|
|
||||||
private final World world;
|
private final World world;
|
||||||
private final Path backupsDir;
|
private final Path backupsDir;
|
||||||
private final Profile profile;
|
private final Profile profile;
|
||||||
private final String id;
|
private final String versionId;
|
||||||
|
private FileChannel sessionLockChannel;
|
||||||
|
|
||||||
private boolean loadFailed = false;
|
private final ObjectProperty<State> state;
|
||||||
|
private boolean isFirstNavigation = true;
|
||||||
|
private final BooleanProperty refreshable = new SimpleBooleanProperty(true);
|
||||||
|
private final BooleanProperty readOnly = new SimpleBooleanProperty(false);
|
||||||
|
|
||||||
private final TabHeader header;
|
private final TransitionPane transitionPane = new TransitionPane();
|
||||||
|
private final TabHeader header = new TabHeader(transitionPane);
|
||||||
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");
|
||||||
private final TabHeader.Tab<DatapackListPage> datapackTab = new TabHeader.Tab<>("datapackListPage");
|
private final TabHeader.Tab<DatapackListPage> datapackTab = new TabHeader.Tab<>("datapackListPage");
|
||||||
|
|
||||||
private final TransitionPane transitionPane = new TransitionPane();
|
public WorldManagePage(World world, Profile profile, String versionId) {
|
||||||
|
|
||||||
private FileChannel sessionLockChannel;
|
|
||||||
|
|
||||||
public WorldManagePage(World world, Path backupsDir, Profile profile, String id) {
|
|
||||||
this.world = world;
|
this.world = world;
|
||||||
this.backupsDir = backupsDir;
|
this.backupsDir = profile.getRepository().getBackupsDirectory(versionId);
|
||||||
this.profile = profile;
|
this.profile = profile;
|
||||||
this.id = id;
|
this.versionId = versionId;
|
||||||
|
|
||||||
|
updateSessionLockChannel();
|
||||||
|
|
||||||
sessionLockChannel = WorldManageUIUtils.getSessionLockChannel(world);
|
|
||||||
try {
|
try {
|
||||||
world.reloadLevelDat();
|
this.world.reloadLevelDat();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOG.warning("Can not load world level.dat of world: " + world.getFile(), e);
|
LOG.warning("Can not load world level.dat of world: " + this.world.getFile(), e);
|
||||||
loadFailed = true;
|
this.addEventHandler(Navigator.NavigationEvent.NAVIGATED, event -> closePageForLoadingFail());
|
||||||
}
|
}
|
||||||
|
|
||||||
this.worldInfoTab.setNodeSupplier(() -> new WorldInfoPage(this));
|
worldInfoTab.setNodeSupplier(() -> new WorldInfoPage(this));
|
||||||
this.worldBackupsTab.setNodeSupplier(() -> new WorldBackupsPage(this));
|
worldBackupsTab.setNodeSupplier(() -> new WorldBackupsPage(this));
|
||||||
this.datapackTab.setNodeSupplier(() -> new DatapackListPage(this));
|
datapackTab.setNodeSupplier(() -> new DatapackListPage(this));
|
||||||
|
|
||||||
this.state = new SimpleObjectProperty<>(State.fromTitle(i18n("world.manage.title", StringUtils.parseColorEscapes(world.getWorldName()))));
|
this.state = new SimpleObjectProperty<>(new State(i18n("world.manage.title", StringUtils.parseColorEscapes(world.getWorldName())), null, true, true, true));
|
||||||
this.header = new TabHeader(transitionPane, worldInfoTab, worldBackupsTab);
|
|
||||||
header.select(worldInfoTab);
|
|
||||||
|
|
||||||
setCenter(transitionPane);
|
|
||||||
|
|
||||||
BorderPane left = new BorderPane();
|
|
||||||
FXUtils.setLimitWidth(left, 200);
|
|
||||||
VBox.setVgrow(left, Priority.ALWAYS);
|
|
||||||
setLeft(left);
|
|
||||||
|
|
||||||
AdvancedListBox sideBar = new AdvancedListBox()
|
|
||||||
.addNavigationDrawerTab(header, worldInfoTab, i18n("world.info"), SVG.INFO, SVG.INFO_FILL)
|
|
||||||
.addNavigationDrawerTab(header, worldBackupsTab, i18n("world.backup"), SVG.ARCHIVE, SVG.ARCHIVE_FILL);
|
|
||||||
|
|
||||||
if (world.getGameVersion() != null && // old game will not write game version to level.dat
|
|
||||||
world.getGameVersion().isAtLeast("1.13", "17w43a")) {
|
|
||||||
header.getTabs().add(datapackTab);
|
|
||||||
sideBar.addNavigationDrawerTab(header, datapackTab, i18n("world.datapack"), SVG.EXTENSION, SVG.EXTENSION_FILL);
|
|
||||||
}
|
|
||||||
|
|
||||||
left.setTop(sideBar);
|
|
||||||
|
|
||||||
AdvancedListBox toolbar = new AdvancedListBox();
|
|
||||||
|
|
||||||
if (world.getGameVersion() != null && world.getGameVersion().isAtLeast("1.20", "23w14a")) {
|
|
||||||
toolbar.addNavigationDrawerItem(i18n("version.launch"), SVG.ROCKET_LAUNCH, this::launch, advancedListItem -> advancedListItem.setDisable(isReadOnly()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ChunkBaseApp.isSupported(world)) {
|
|
||||||
PopupMenu chunkBasePopupMenu = new PopupMenu();
|
|
||||||
JFXPopup chunkBasePopup = new JFXPopup(chunkBasePopupMenu);
|
|
||||||
|
|
||||||
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) {
|
|
||||||
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 ->
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (world.getGameVersion() != null && world.getGameVersion().isAtLeast("1.20", "23w14a")) {
|
|
||||||
managePopupMenu.getContent().addAll(
|
|
||||||
new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch"), this::launch, managePopup),
|
|
||||||
new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), this::generateLaunchScript, managePopup),
|
|
||||||
new MenuSeparator()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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.EXITED, this::onExited);
|
||||||
this.addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
|
this.addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onNavigated(Navigator.NavigationEvent event) {
|
@Override
|
||||||
if (loadFailed) {
|
protected @NotNull Skin createDefaultSkin() {
|
||||||
|
return new Skin(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void refresh() {
|
||||||
|
updateSessionLockChannel();
|
||||||
|
try {
|
||||||
|
world.reloadLevelDat();
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.warning("Can not load world level.dat of world: " + world.getFile(), e);
|
||||||
|
closePageForLoadingFail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var tab : header.getTabs()) {
|
||||||
|
if (tab.getNode() instanceof WorldRefreshable r) {
|
||||||
|
r.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closePageForLoadingFail() {
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
fireEvent(new PageCloseEvent());
|
fireEvent(new PageCloseEvent());
|
||||||
Controllers.dialog(i18n("world.load.fail"), null, MessageDialogPane.MessageType.ERROR);
|
Controllers.dialog(i18n("world.load.fail"), null, MessageDialogPane.MessageType.ERROR);
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateSessionLockChannel() {
|
||||||
if (sessionLockChannel == null || !sessionLockChannel.isOpen()) {
|
if (sessionLockChannel == null || !sessionLockChannel.isOpen()) {
|
||||||
sessionLockChannel = WorldManageUIUtils.getSessionLockChannel(world);
|
sessionLockChannel = WorldManageUIUtils.getSessionLockChannel(world);
|
||||||
|
readOnly.set(sessionLockChannel == null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onNavigated(Navigator.NavigationEvent event) {
|
||||||
|
if (isFirstNavigation)
|
||||||
|
isFirstNavigation = false;
|
||||||
|
else
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
public void onExited(Navigator.NavigationEvent event) {
|
public void onExited(Navigator.NavigationEvent event) {
|
||||||
try {
|
try {
|
||||||
WorldManageUIUtils.closeSessionLockChannel(world, sessionLockChannel);
|
WorldManageUIUtils.closeSessionLockChannel(world, sessionLockChannel);
|
||||||
@@ -194,11 +142,24 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void launch() {
|
||||||
|
fireEvent(new PageCloseEvent());
|
||||||
|
Versions.launchAndEnterWorld(profile, versionId, world.getFileName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void generateLaunchScript() {
|
||||||
|
Versions.generateLaunchScriptForQuickEnterWorld(profile, versionId, world.getFileName());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ReadOnlyObjectProperty<State> stateProperty() {
|
public ReadOnlyObjectProperty<State> stateProperty() {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title) {
|
||||||
|
this.state.set(new DecoratorPage.State(title, null, true, true, true));
|
||||||
|
}
|
||||||
|
|
||||||
public World getWorld() {
|
public World getWorld() {
|
||||||
return world;
|
return world;
|
||||||
}
|
}
|
||||||
@@ -208,15 +169,123 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isReadOnly() {
|
public boolean isReadOnly() {
|
||||||
return sessionLockChannel == null;
|
return readOnly.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void launch() {
|
public BooleanProperty readOnlyProperty() {
|
||||||
fireEvent(new PageCloseEvent());
|
return readOnly;
|
||||||
Versions.launchAndEnterWorld(profile, id, world.getFileName());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void generateLaunchScript() {
|
@Override
|
||||||
Versions.generateLaunchScriptForQuickEnterWorld(profile, id, world.getFileName());
|
public BooleanProperty refreshableProperty() {
|
||||||
|
return refreshable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Skin extends DecoratorAnimatedPageSkin<WorldManagePage> {
|
||||||
|
|
||||||
|
protected Skin(WorldManagePage control) {
|
||||||
|
super(control);
|
||||||
|
|
||||||
|
setCenter(control.transitionPane);
|
||||||
|
setLeft(getSidebar());
|
||||||
|
}
|
||||||
|
|
||||||
|
private BorderPane getSidebar() {
|
||||||
|
BorderPane sidebar = new BorderPane();
|
||||||
|
{
|
||||||
|
FXUtils.setLimitWidth(sidebar, 200);
|
||||||
|
VBox.setVgrow(sidebar, Priority.ALWAYS);
|
||||||
|
}
|
||||||
|
|
||||||
|
sidebar.setTop(getTabBar());
|
||||||
|
sidebar.setBottom(getToolBar());
|
||||||
|
|
||||||
|
return sidebar;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AdvancedListBox getTabBar() {
|
||||||
|
AdvancedListBox tabBar = new AdvancedListBox();
|
||||||
|
{
|
||||||
|
getSkinnable().header.getTabs().addAll(getSkinnable().worldInfoTab, getSkinnable().worldBackupsTab);
|
||||||
|
getSkinnable().header.select(getSkinnable().worldInfoTab);
|
||||||
|
|
||||||
|
tabBar.addNavigationDrawerTab(getSkinnable().header, getSkinnable().worldInfoTab, i18n("world.info"), SVG.INFO, SVG.INFO_FILL)
|
||||||
|
.addNavigationDrawerTab(getSkinnable().header, getSkinnable().worldBackupsTab, i18n("world.backup"), SVG.ARCHIVE, SVG.ARCHIVE_FILL);
|
||||||
|
|
||||||
|
if (getSkinnable().world.supportDatapacks()) {
|
||||||
|
getSkinnable().header.getTabs().add(getSkinnable().datapackTab);
|
||||||
|
tabBar.addNavigationDrawerTab(getSkinnable().header, getSkinnable().datapackTab, i18n("world.datapack"), SVG.EXTENSION, SVG.EXTENSION_FILL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tabBar;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AdvancedListBox getToolBar() {
|
||||||
|
AdvancedListBox toolbar = new AdvancedListBox();
|
||||||
|
BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0));
|
||||||
|
{
|
||||||
|
if (getSkinnable().world.supportQuickPlay()) {
|
||||||
|
toolbar.addNavigationDrawerItem(i18n("version.launch"), SVG.ROCKET_LAUNCH, () -> getSkinnable().launch(), advancedListItem -> advancedListItem.disableProperty().bind(getSkinnable().readOnlyProperty()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ChunkBaseApp.isSupported(getSkinnable().world)) {
|
||||||
|
PopupMenu chunkBasePopupMenu = new PopupMenu();
|
||||||
|
JFXPopup chunkBasePopup = new JFXPopup(chunkBasePopupMenu);
|
||||||
|
|
||||||
|
chunkBasePopupMenu.getContent().addAll(
|
||||||
|
new IconedMenuItem(SVG.EXPLORE, i18n("world.chunkbase.seed_map"), () -> ChunkBaseApp.openSeedMap(getSkinnable().world), chunkBasePopup),
|
||||||
|
new IconedMenuItem(SVG.VISIBILITY, i18n("world.chunkbase.stronghold"), () -> ChunkBaseApp.openStrongholdFinder(getSkinnable().world), chunkBasePopup),
|
||||||
|
new IconedMenuItem(SVG.FORT, i18n("world.chunkbase.nether_fortress"), () -> ChunkBaseApp.openNetherFortressFinder(getSkinnable().world), chunkBasePopup)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ChunkBaseApp.supportEndCity(getSkinnable().world)) {
|
||||||
|
chunkBasePopupMenu.getContent().add(
|
||||||
|
new IconedMenuItem(SVG.LOCATION_CITY, i18n("world.chunkbase.end_city"), () -> ChunkBaseApp.openEndCityFinder(getSkinnable().world), chunkBasePopup));
|
||||||
|
}
|
||||||
|
|
||||||
|
toolbar.addNavigationDrawerItem(i18n("world.chunkbase"), SVG.EXPLORE, null, chunkBaseMenuItem ->
|
||||||
|
chunkBaseMenuItem.setOnAction(e ->
|
||||||
|
chunkBasePopup.show(chunkBaseMenuItem,
|
||||||
|
JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT,
|
||||||
|
chunkBaseMenuItem.getWidth(), 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
toolbar.addNavigationDrawerItem(i18n("settings.game.exploration"), SVG.FOLDER_OPEN, () -> FXUtils.openFolder(getSkinnable().world.getFile()));
|
||||||
|
|
||||||
|
{
|
||||||
|
PopupMenu managePopupMenu = new PopupMenu();
|
||||||
|
JFXPopup managePopup = new JFXPopup(managePopupMenu);
|
||||||
|
|
||||||
|
if (getSkinnable().world.supportQuickPlay()) {
|
||||||
|
managePopupMenu.getContent().addAll(
|
||||||
|
new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch"), () -> getSkinnable().launch(), managePopup),
|
||||||
|
new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), () -> getSkinnable().generateLaunchScript(), managePopup),
|
||||||
|
new MenuSeparator()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
managePopupMenu.getContent().addAll(
|
||||||
|
new IconedMenuItem(SVG.OUTPUT, i18n("world.export"), () -> WorldManageUIUtils.export(getSkinnable().world, getSkinnable().sessionLockChannel), managePopup),
|
||||||
|
new IconedMenuItem(SVG.DELETE, i18n("world.delete"), () -> WorldManageUIUtils.delete(getSkinnable().world, () -> getSkinnable().fireEvent(new PageCloseEvent()), getSkinnable().sessionLockChannel), managePopup),
|
||||||
|
new IconedMenuItem(SVG.CONTENT_COPY, i18n("world.duplicate"), () -> WorldManageUIUtils.copyWorld(getSkinnable().world, null), managePopup)
|
||||||
|
);
|
||||||
|
|
||||||
|
toolbar.addNavigationDrawerItem(i18n("settings.game.management"), SVG.MENU, null, managePopupMenuItem ->
|
||||||
|
{
|
||||||
|
managePopupMenuItem.setOnAction(e ->
|
||||||
|
managePopup.show(managePopupMenuItem,
|
||||||
|
JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT,
|
||||||
|
managePopupMenuItem.getWidth(), 0));
|
||||||
|
managePopupMenuItem.disableProperty().bind(getSkinnable().readOnlyProperty());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return toolbar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface WorldRefreshable {
|
||||||
|
void refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ public final class WorldManageUIUtils {
|
|||||||
FileChannel lock = world.lock();
|
FileChannel lock = world.lock();
|
||||||
LOG.info("Acquired lock on world " + world.getFileName());
|
LOG.info("Acquired lock on world " + world.getFileName());
|
||||||
return lock;
|
return lock;
|
||||||
} catch (IOException ignored) {
|
} catch (WorldLockedException ignored) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ public final class ChunkBaseApp {
|
|||||||
private static final String CHUNK_BASE_URL = "https://www.chunkbase.com";
|
private static final String CHUNK_BASE_URL = "https://www.chunkbase.com";
|
||||||
|
|
||||||
private static final GameVersionNumber MIN_GAME_VERSION = GameVersionNumber.asGameVersion("1.7");
|
private static final GameVersionNumber MIN_GAME_VERSION = GameVersionNumber.asGameVersion("1.7");
|
||||||
|
private static final GameVersionNumber MIN_END_CITY_VERSION = GameVersionNumber.asGameVersion("1.13");
|
||||||
|
|
||||||
private static final String[] SEED_MAP_GAME_VERSIONS = {
|
private static final String[] SEED_MAP_GAME_VERSIONS = {
|
||||||
"1.21.9", "1.21.6", "1.21.5", "1.21.4", "1.21.2", "1.21", "1.20",
|
"1.21.9", "1.21.6", "1.21.5", "1.21.4", "1.21.2", "1.21", "1.20",
|
||||||
@@ -52,6 +53,11 @@ public final class ChunkBaseApp {
|
|||||||
world.getGameVersion().compareTo(MIN_GAME_VERSION) >= 0;
|
world.getGameVersion().compareTo(MIN_GAME_VERSION) >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean supportEndCity(@NotNull World world) {
|
||||||
|
return world.getSeed() != null && world.getGameVersion() != null &&
|
||||||
|
world.getGameVersion().compareTo(MIN_END_CITY_VERSION) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
public static ChunkBaseApp newBuilder(String app, long seed) {
|
public static ChunkBaseApp newBuilder(String app, long seed) {
|
||||||
return new ChunkBaseApp(new StringBuilder(CHUNK_BASE_URL).append("/apps/").append(app).append("#seed=").append(seed));
|
return new ChunkBaseApp(new StringBuilder(CHUNK_BASE_URL).append("/apps/").append(app).append("#seed=").append(seed));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1130,6 +1130,7 @@ datapack=Datapacks
|
|||||||
datapack.add=Install Datapack
|
datapack.add=Install Datapack
|
||||||
datapack.choose_datapack=Choose datapack to import
|
datapack.choose_datapack=Choose datapack to import
|
||||||
datapack.extension=Datapack
|
datapack.extension=Datapack
|
||||||
|
datapack.reload.toast=Minecraft is running, please use the /reload command to reload the data pack
|
||||||
datapack.title=World [%s] - Datapacks
|
datapack.title=World [%s] - Datapacks
|
||||||
|
|
||||||
web.failed=Failed to load page
|
web.failed=Failed to load page
|
||||||
@@ -1198,6 +1199,7 @@ world.info.last_played=Last Played
|
|||||||
world.info.generate_features=Generate Structures
|
world.info.generate_features=Generate Structures
|
||||||
world.info.player=Player Information
|
world.info.player=Player Information
|
||||||
world.info.player.food_level=Hunger Level
|
world.info.player.food_level=Hunger Level
|
||||||
|
world.info.player.food_saturation_level=Saturation
|
||||||
world.info.player.game_type=Game Mode
|
world.info.player.game_type=Game Mode
|
||||||
world.info.player.game_type.adventure=Adventure
|
world.info.player.game_type.adventure=Adventure
|
||||||
world.info.player.game_type.creative=Creative
|
world.info.player.game_type.creative=Creative
|
||||||
@@ -1210,8 +1212,9 @@ world.info.player.location=Location
|
|||||||
world.info.player.spawn=Spawn Location
|
world.info.player.spawn=Spawn Location
|
||||||
world.info.player.xp_level=Experience Level
|
world.info.player.xp_level=Experience Level
|
||||||
world.info.random_seed=Seed
|
world.info.random_seed=Seed
|
||||||
world.info.time=Game Time
|
world.info.spawn=World Spawn Location
|
||||||
world.info.time.format=%s days
|
world.info.time=Played Time
|
||||||
|
world.info.time.format=%dd %dh %dm
|
||||||
world.load.fail=Failed to load world
|
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.
|
||||||
|
|||||||
@@ -1153,8 +1153,6 @@ world.info.player.location=الموقع
|
|||||||
world.info.player.spawn=موقع الظهور
|
world.info.player.spawn=موقع الظهور
|
||||||
world.info.player.xp_level=مستوى الخبرة
|
world.info.player.xp_level=مستوى الخبرة
|
||||||
world.info.random_seed=البذرة
|
world.info.random_seed=البذرة
|
||||||
world.info.time=وقت اللعبة
|
|
||||||
world.info.time.format=%s أيام
|
|
||||||
world.locked=قيد الاستخدام
|
world.locked=قيد الاستخدام
|
||||||
world.locked.failed=العالم قيد الاستخدام حاليًا. يرجى إغلاق اللعبة والمحاولة مرة أخرى.
|
world.locked.failed=العالم قيد الاستخدام حاليًا. يرجى إغلاق اللعبة والمحاولة مرة أخرى.
|
||||||
world.manage=العوالم
|
world.manage=العوالم
|
||||||
|
|||||||
@@ -1162,8 +1162,6 @@ world.info.player.location=Ubicación
|
|||||||
world.info.player.spawn=Ubicación de desove
|
world.info.player.spawn=Ubicación de desove
|
||||||
world.info.player.xp_level=Nivel de experiencia
|
world.info.player.xp_level=Nivel de experiencia
|
||||||
world.info.random_seed=Semilla
|
world.info.random_seed=Semilla
|
||||||
world.info.time=Tiempo de juego
|
|
||||||
world.info.time.format=%s días
|
|
||||||
world.locked=En uso
|
world.locked=En uso
|
||||||
world.locked.failed=El mundo está actualmente en uso. Por favor, cierra el juego e inténtalo de nuevo.
|
world.locked.failed=El mundo está actualmente en uso. Por favor, cierra el juego e inténtalo de nuevo.
|
||||||
world.manage=Mundos
|
world.manage=Mundos
|
||||||
|
|||||||
@@ -951,8 +951,6 @@ world.info.player.location=所
|
|||||||
world.info.player.spawn=床/復生錨之所
|
world.info.player.spawn=床/復生錨之所
|
||||||
world.info.player.xp_level=經驗之層
|
world.info.player.xp_level=經驗之層
|
||||||
world.info.random_seed=種
|
world.info.random_seed=種
|
||||||
world.info.time=戲之時辰
|
|
||||||
world.info.time.format=%s 日
|
|
||||||
world.locked=見用
|
world.locked=見用
|
||||||
world.manage=司生界
|
world.manage=司生界
|
||||||
world.manage.button=司生界
|
world.manage.button=司生界
|
||||||
|
|||||||
@@ -1154,8 +1154,6 @@ world.info.player.location=Расположение
|
|||||||
world.info.player.spawn=Точка возрождения
|
world.info.player.spawn=Точка возрождения
|
||||||
world.info.player.xp_level=Уровень опыта
|
world.info.player.xp_level=Уровень опыта
|
||||||
world.info.random_seed=Ключ генератора мира
|
world.info.random_seed=Ключ генератора мира
|
||||||
world.info.time=Время игры
|
|
||||||
world.info.time.format=%s дн.
|
|
||||||
world.locked=В эксплуатации
|
world.locked=В эксплуатации
|
||||||
world.locked.failed=В настоящее время мир находится в эксплуатации. Закройте игру и попробуйте снова.
|
world.locked.failed=В настоящее время мир находится в эксплуатации. Закройте игру и попробуйте снова.
|
||||||
world.manage=Миры
|
world.manage=Миры
|
||||||
|
|||||||
@@ -1100,8 +1100,6 @@ world.info.player.location=Місцезнаходження
|
|||||||
world.info.player.spawn=Місце появи
|
world.info.player.spawn=Місце появи
|
||||||
world.info.player.xp_level=Рівень досвіду
|
world.info.player.xp_level=Рівень досвіду
|
||||||
world.info.random_seed=Насіння
|
world.info.random_seed=Насіння
|
||||||
world.info.time=Час гри
|
|
||||||
world.info.time.format=%s днів
|
|
||||||
world.locked=Використовується
|
world.locked=Використовується
|
||||||
world.locked.failed=Світ наразі використовується. Закрийте гру та спробуйте знову.
|
world.locked.failed=Світ наразі використовується. Закрийте гру та спробуйте знову.
|
||||||
world.manage=Світи
|
world.manage=Світи
|
||||||
|
|||||||
@@ -919,6 +919,7 @@ datapack=資料包
|
|||||||
datapack.add=新增資料包
|
datapack.add=新增資料包
|
||||||
datapack.choose_datapack=選取要匯入的資料包壓縮檔
|
datapack.choose_datapack=選取要匯入的資料包壓縮檔
|
||||||
datapack.extension=資料包
|
datapack.extension=資料包
|
||||||
|
datapack.reload.toast=Minecraft 正在執行,請使用 /reload 指令重新載入資料包
|
||||||
datapack.title=世界 [%s] - 資料包
|
datapack.title=世界 [%s] - 資料包
|
||||||
|
|
||||||
web.failed=載入頁面失敗
|
web.failed=載入頁面失敗
|
||||||
@@ -986,6 +987,7 @@ world.info.last_played=上一次遊戲時間
|
|||||||
world.info.generate_features=生成建築
|
world.info.generate_features=生成建築
|
||||||
world.info.player=玩家資訊
|
world.info.player=玩家資訊
|
||||||
world.info.player.food_level=饑餓值
|
world.info.player.food_level=饑餓值
|
||||||
|
world.info.player.food_saturation_level=飽食度
|
||||||
world.info.player.game_type=遊戲模式
|
world.info.player.game_type=遊戲模式
|
||||||
world.info.player.game_type.adventure=冒險
|
world.info.player.game_type.adventure=冒險
|
||||||
world.info.player.game_type.creative=創造
|
world.info.player.game_type.creative=創造
|
||||||
@@ -998,8 +1000,9 @@ world.info.player.location=位置
|
|||||||
world.info.player.spawn=床/重生錨位置
|
world.info.player.spawn=床/重生錨位置
|
||||||
world.info.player.xp_level=經驗等級
|
world.info.player.xp_level=經驗等級
|
||||||
world.info.random_seed=種子碼
|
world.info.random_seed=種子碼
|
||||||
world.info.time=遊戲內時間
|
world.info.spawn=世界重生點
|
||||||
world.info.time.format=%s 天
|
world.info.time=遊戲時間
|
||||||
|
world.info.time.format=%d 天 %d 小時 %d 分鐘
|
||||||
world.load.fail=世界載入失敗
|
world.load.fail=世界載入失敗
|
||||||
world.locked=使用中
|
world.locked=使用中
|
||||||
world.locked.failed=該世界正在使用中,請關閉遊戲後重試。
|
world.locked.failed=該世界正在使用中,請關閉遊戲後重試。
|
||||||
|
|||||||
@@ -924,6 +924,7 @@ datapack=数据包
|
|||||||
datapack.add=添加数据包
|
datapack.add=添加数据包
|
||||||
datapack.choose_datapack=选择要导入的数据包压缩包
|
datapack.choose_datapack=选择要导入的数据包压缩包
|
||||||
datapack.extension=数据包
|
datapack.extension=数据包
|
||||||
|
datapack.reload.toast=Minecraft 正在运行,请使用 /reload 命令重新加载数据包
|
||||||
datapack.title=世界 [%s] - 数据包
|
datapack.title=世界 [%s] - 数据包
|
||||||
|
|
||||||
web.failed=加载页面失败
|
web.failed=加载页面失败
|
||||||
@@ -992,6 +993,7 @@ world.info.last_played=上一次游戏时间
|
|||||||
world.info.generate_features=生成建筑
|
world.info.generate_features=生成建筑
|
||||||
world.info.player=玩家信息
|
world.info.player=玩家信息
|
||||||
world.info.player.food_level=饥饿值
|
world.info.player.food_level=饥饿值
|
||||||
|
world.info.player.food_saturation_level=饱和度
|
||||||
world.info.player.game_type=游戏模式
|
world.info.player.game_type=游戏模式
|
||||||
world.info.player.game_type.adventure=冒险
|
world.info.player.game_type.adventure=冒险
|
||||||
world.info.player.game_type.creative=创造
|
world.info.player.game_type.creative=创造
|
||||||
@@ -1004,8 +1006,9 @@ world.info.player.location=位置
|
|||||||
world.info.player.spawn=床/重生锚位置
|
world.info.player.spawn=床/重生锚位置
|
||||||
world.info.player.xp_level=经验等级
|
world.info.player.xp_level=经验等级
|
||||||
world.info.random_seed=种子
|
world.info.random_seed=种子
|
||||||
world.info.time=游戏内时间
|
world.info.spawn=世界出生点
|
||||||
world.info.time.format=%s 天
|
world.info.time=游戏时长
|
||||||
|
world.info.time.format=%d 天 %d 小时 %d 分钟
|
||||||
world.load.fail=世界加载失败
|
world.load.fail=世界加载失败
|
||||||
world.locked=使用中
|
world.locked=使用中
|
||||||
world.locked.failed=该世界正在使用中,请关闭游戏后重试。
|
world.locked.failed=该世界正在使用中,请关闭游戏后重试。
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import java.nio.channels.OverlappingFileLockException;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.*;
|
import java.nio.file.*;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import java.util.zip.GZIPInputStream;
|
import java.util.zip.GZIPInputStream;
|
||||||
import java.util.zip.GZIPOutputStream;
|
import java.util.zip.GZIPOutputStream;
|
||||||
@@ -47,7 +46,6 @@ public final class World {
|
|||||||
private String fileName;
|
private String fileName;
|
||||||
private CompoundTag levelData;
|
private CompoundTag levelData;
|
||||||
private Image icon;
|
private Image icon;
|
||||||
private boolean isLocked;
|
|
||||||
private Path levelDataPath;
|
private Path levelDataPath;
|
||||||
|
|
||||||
public World(Path file) throws IOException {
|
public World(Path file) throws IOException {
|
||||||
@@ -144,7 +142,19 @@ public final class World {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isLocked() {
|
public boolean isLocked() {
|
||||||
return isLocked;
|
return isLocked(getSessionLockFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean supportDatapacks() {
|
||||||
|
return getGameVersion() != null && getGameVersion().isAtLeast("1.13", "17w43a");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean supportQuickPlay() {
|
||||||
|
return getGameVersion() != null && getGameVersion().isAtLeast("1.20", "23w14a");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean supportQuickPlay(GameVersionNumber gameVersionNumber) {
|
||||||
|
return gameVersionNumber != null && gameVersionNumber.isAtLeast("1.20", "23w14a");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadFromDirectory() throws IOException {
|
private void loadFromDirectory() throws IOException {
|
||||||
@@ -153,9 +163,11 @@ public final class World {
|
|||||||
if (!Files.exists(levelDat)) { // version 20w14infinite
|
if (!Files.exists(levelDat)) { // version 20w14infinite
|
||||||
levelDat = file.resolve("special_level.dat");
|
levelDat = file.resolve("special_level.dat");
|
||||||
}
|
}
|
||||||
|
if (!Files.exists(levelDat)) {
|
||||||
|
throw new IOException("Not a valid world directory since level.dat or special_level.dat cannot be found.");
|
||||||
|
}
|
||||||
loadAndCheckLevelDat(levelDat);
|
loadAndCheckLevelDat(levelDat);
|
||||||
this.levelDataPath = levelDat;
|
this.levelDataPath = levelDat;
|
||||||
isLocked = isLocked(getSessionLockFile());
|
|
||||||
|
|
||||||
Path iconFile = file.resolve("icon.png");
|
Path iconFile = file.resolve("icon.png");
|
||||||
if (Files.isRegularFile(iconFile)) {
|
if (Files.isRegularFile(iconFile)) {
|
||||||
@@ -177,7 +189,6 @@ public final class World {
|
|||||||
if (!Files.exists(levelDat)) {
|
if (!Files.exists(levelDat)) {
|
||||||
throw new IOException("Not a valid world zip file since level.dat or special_level.dat cannot be found.");
|
throw new IOException("Not a valid world zip file since level.dat or special_level.dat cannot be found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
loadAndCheckLevelDat(levelDat);
|
loadAndCheckLevelDat(levelDat);
|
||||||
|
|
||||||
Path iconFile = root.resolve("icon.png");
|
Path iconFile = root.resolve("icon.png");
|
||||||
@@ -193,10 +204,9 @@ public final class World {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void loadFromZip() throws IOException {
|
private void loadFromZip() throws IOException {
|
||||||
isLocked = false;
|
|
||||||
try (FileSystem fs = CompressingUtils.readonly(file).setAutoDetectEncoding(true).build()) {
|
try (FileSystem fs = CompressingUtils.readonly(file).setAutoDetectEncoding(true).build()) {
|
||||||
Path cur = fs.getPath("/level.dat");
|
Path levelDatPath = fs.getPath("/level.dat");
|
||||||
if (Files.isRegularFile(cur)) {
|
if (Files.isRegularFile(levelDatPath)) {
|
||||||
fileName = FileUtils.getName(file);
|
fileName = FileUtils.getName(file);
|
||||||
loadFromZipImpl(fs.getPath("/"));
|
loadFromZipImpl(fs.getPath("/"));
|
||||||
return;
|
return;
|
||||||
@@ -230,6 +240,8 @@ public final class World {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The rename method is used to rename temporary world object during installation and copying,
|
||||||
|
// so there is no need to modify the `file` field.
|
||||||
public void rename(String newName) throws IOException {
|
public void rename(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");
|
||||||
@@ -257,14 +269,14 @@ public final class World {
|
|||||||
|
|
||||||
if (Files.isRegularFile(file)) {
|
if (Files.isRegularFile(file)) {
|
||||||
try (FileSystem fs = CompressingUtils.readonly(file).setAutoDetectEncoding(true).build()) {
|
try (FileSystem fs = CompressingUtils.readonly(file).setAutoDetectEncoding(true).build()) {
|
||||||
Path cur = fs.getPath("/level.dat");
|
Path levelDatPath = fs.getPath("/level.dat");
|
||||||
if (Files.isRegularFile(cur)) {
|
if (Files.isRegularFile(levelDatPath)) {
|
||||||
fileName = FileUtils.getName(file);
|
fileName = FileUtils.getName(file);
|
||||||
|
|
||||||
new Unzipper(file, worldDir).unzip();
|
new Unzipper(file, worldDir).unzip();
|
||||||
} else {
|
} else {
|
||||||
try (Stream<Path> stream = Files.list(fs.getPath("/"))) {
|
try (Stream<Path> stream = Files.list(fs.getPath("/"))) {
|
||||||
List<Path> subDirs = stream.collect(Collectors.toList());
|
List<Path> subDirs = stream.toList();
|
||||||
if (subDirs.size() != 1) {
|
if (subDirs.size() != 1) {
|
||||||
throw new IOException("World zip malformed");
|
throw new IOException("World zip malformed");
|
||||||
}
|
}
|
||||||
@@ -347,8 +359,8 @@ public final class World {
|
|||||||
private static CompoundTag parseLevelDat(Path path) throws IOException {
|
private static CompoundTag parseLevelDat(Path path) throws IOException {
|
||||||
try (InputStream is = new GZIPInputStream(Files.newInputStream(path))) {
|
try (InputStream is = new GZIPInputStream(Files.newInputStream(path))) {
|
||||||
Tag nbt = NBTIO.readTag(is);
|
Tag nbt = NBTIO.readTag(is);
|
||||||
if (nbt instanceof CompoundTag)
|
if (nbt instanceof CompoundTag compoundTag)
|
||||||
return (CompoundTag) nbt;
|
return compoundTag;
|
||||||
else
|
else
|
||||||
throw new IOException("level.dat malformed");
|
throw new IOException("level.dat malformed");
|
||||||
}
|
}
|
||||||
@@ -367,21 +379,21 @@ public final class World {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Stream<World> getWorlds(Path savesDir) {
|
public static List<World> getWorlds(Path savesDir) {
|
||||||
try {
|
|
||||||
if (Files.exists(savesDir)) {
|
if (Files.exists(savesDir)) {
|
||||||
return Files.list(savesDir).flatMap(world -> {
|
try (Stream<Path> stream = Files.list(savesDir)) {
|
||||||
|
return stream.flatMap(world -> {
|
||||||
try {
|
try {
|
||||||
return Stream.of(new World(world.toAbsolutePath()));
|
return Stream.of(new World(world.toAbsolutePath().normalize()));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOG.warning("Failed to read world " + world, e);
|
LOG.warning("Failed to read world " + world, e);
|
||||||
return Stream.empty();
|
return Stream.empty();
|
||||||
}
|
}
|
||||||
});
|
}).toList();
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOG.warning("Failed to read saves", e);
|
LOG.warning("Failed to read saves", e);
|
||||||
}
|
}
|
||||||
return Stream.empty();
|
}
|
||||||
|
return List.of();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -322,7 +322,7 @@ public class DefaultLauncher extends Launcher {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
ServerAddress parsed = ServerAddress.parse(address);
|
ServerAddress parsed = ServerAddress.parse(address);
|
||||||
if (GameVersionNumber.asGameVersion(gameVersion).isAtLeast("1.20", "23w14a")) {
|
if (World.supportQuickPlay(GameVersionNumber.asGameVersion(gameVersion))) {
|
||||||
res.add("--quickPlayMultiplayer");
|
res.add("--quickPlayMultiplayer");
|
||||||
res.add(parsed.getPort() >= 0 ? address : parsed.getHost() + ":25565");
|
res.add(parsed.getPort() >= 0 ? address : parsed.getHost() + ":25565");
|
||||||
} else {
|
} else {
|
||||||
@@ -335,11 +335,11 @@ public class DefaultLauncher extends Launcher {
|
|||||||
LOG.warning("Invalid server address: " + address, e);
|
LOG.warning("Invalid server address: " + address, e);
|
||||||
}
|
}
|
||||||
} else if (options.getQuickPlayOption() instanceof QuickPlayOption.SinglePlayer singlePlayer
|
} else if (options.getQuickPlayOption() instanceof QuickPlayOption.SinglePlayer singlePlayer
|
||||||
&& GameVersionNumber.asGameVersion(gameVersion).isAtLeast("1.20", "23w14a")) {
|
&& World.supportQuickPlay(GameVersionNumber.asGameVersion(gameVersion))) {
|
||||||
res.add("--quickPlaySingleplayer");
|
res.add("--quickPlaySingleplayer");
|
||||||
res.add(singlePlayer.worldFolderName());
|
res.add(singlePlayer.worldFolderName());
|
||||||
} else if (options.getQuickPlayOption() instanceof QuickPlayOption.Realm realm
|
} else if (options.getQuickPlayOption() instanceof QuickPlayOption.Realm realm
|
||||||
&& GameVersionNumber.asGameVersion(gameVersion).isAtLeast("1.20", "23w14a")) {
|
&& World.supportQuickPlay(GameVersionNumber.asGameVersion(gameVersion))) {
|
||||||
res.add("--quickPlayRealms");
|
res.add("--quickPlayRealms");
|
||||||
res.add(realm.realmID());
|
res.add(realm.realmID());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -280,6 +280,15 @@ public final class Lang {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Float toFloatOrNull(Object string) {
|
||||||
|
try {
|
||||||
|
if (string == null) return null;
|
||||||
|
return Float.parseFloat(string.toString());
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the first non-null reference in given list.
|
* Find the first non-null reference in given list.
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user