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