Add world info page (#1620)

This commit is contained in:
Glavo 2022-08-08 16:56:03 +08:00 committed by GitHub
parent c627ca96c9
commit 38d71fd6bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 757 additions and 12 deletions

View File

@ -0,0 +1,57 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.NamedArg;
import javafx.scene.control.TextInputControl;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
public class DoubleValidator extends ValidatorBase {
private final boolean nullable;
public DoubleValidator() {
this(false);
}
public DoubleValidator(@NamedArg("nullable") boolean nullable) {
this.nullable = nullable;
}
public DoubleValidator(@NamedArg("message") String message, @NamedArg("nullable") boolean nullable) {
super(message);
this.nullable = nullable;
}
@Override
protected void eval() {
if (srcControl.get() instanceof TextInputControl) {
evalTextInputField();
}
}
private void evalTextInputField() {
TextInputControl textField = ((TextInputControl) srcControl.get());
if (StringUtils.isBlank(textField.getText()))
hasErrors.set(!nullable);
else
hasErrors.set(Lang.toDoubleOrNull(textField.getText()) == null);
}
}

View File

@ -0,0 +1,553 @@
package org.jackhuang.hmcl.ui.versions;
import com.github.steveice10.opennbt.tag.builtin.*;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.DoubleValidator;
import org.jackhuang.hmcl.ui.construct.NumberValidator;
import org.jackhuang.hmcl.ui.construct.OptionToggleButton;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.i18n.Locales;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Locale;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class WorldInfoPage extends StackPane implements DecoratorPage {
private final World world;
private final CompoundTag levelDat;
private final CompoundTag dataTag;
private final ObjectProperty<State> stateProperty = new SimpleObjectProperty<>();
public WorldInfoPage(World world) throws IOException {
this.world = world;
this.levelDat = world.readLevelDat();
this.dataTag = levelDat.get("Data");
CompoundTag worldGenSettings = dataTag.get("WorldGenSettings");
stateProperty.set(State.fromTitle(i18n("world.info.title", world.getWorldName())));
ScrollPane scrollPane = new ScrollPane();
scrollPane.setFitToHeight(true);
scrollPane.setFitToWidth(true);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);
getChildren().setAll(scrollPane);
VBox rootPane = new VBox();
rootPane.setFillWidth(true);
scrollPane.setContent(rootPane);
FXUtils.smoothScrolling(scrollPane);
rootPane.getStyleClass().add("card-list");
ComponentList basicInfo = new ComponentList();
{
BorderPane worldNamePane = new BorderPane();
{
Label label = new Label(i18n("world.name"));
worldNamePane.setLeft(label);
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
Label worldNameLabel = new Label();
worldNameLabel.setText(world.getWorldName());
BorderPane.setAlignment(worldNameLabel, Pos.CENTER_RIGHT);
worldNamePane.setRight(worldNameLabel);
}
BorderPane gameVersionPane = new BorderPane();
{
Label label = new Label(i18n("world.info.game_version"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
gameVersionPane.setLeft(label);
Label gameVersionLabel = new Label();
gameVersionLabel.setText(world.getGameVersion());
BorderPane.setAlignment(gameVersionLabel, Pos.CENTER_RIGHT);
gameVersionPane.setRight(gameVersionLabel);
}
BorderPane randomSeedPane = new BorderPane();
{
Label label = new Label(i18n("world.info.random_seed"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
randomSeedPane.setLeft(label);
Label randomSeedLabel = new Label();
BorderPane.setAlignment(randomSeedLabel, Pos.CENTER_RIGHT);
randomSeedPane.setRight(randomSeedLabel);
Tag tag = worldGenSettings != null ? worldGenSettings.get("seed") : dataTag.get("RandomSeed");
if (tag instanceof LongTag) {
randomSeedLabel.setText(tag.getValue().toString());
}
}
BorderPane lastPlayedPane = new BorderPane();
{
Label label = new Label(i18n("world.info.last_played"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
lastPlayedPane.setLeft(label);
Label lastPlayedLabel = new Label();
lastPlayedLabel.setText(Locales.SIMPLE_DATE_FORMAT.get().format(new Date(world.getLastPlayed())));
BorderPane.setAlignment(lastPlayedLabel, Pos.CENTER_RIGHT);
lastPlayedPane.setRight(lastPlayedLabel);
}
BorderPane timePane = new BorderPane();
{
Label label = new Label(i18n("world.info.time"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
timePane.setLeft(label);
Label timeLabel = new Label();
BorderPane.setAlignment(timeLabel, Pos.CENTER_RIGHT);
timePane.setRight(timeLabel);
Tag tag = dataTag.get("Time");
if (tag instanceof LongTag) {
long days = ((LongTag) tag).getValue() / 24000;
timeLabel.setText(i18n("world.info.time.format", days));
}
}
OptionToggleButton allowCheatsButton = new OptionToggleButton();
{
allowCheatsButton.setTitle(i18n("world.info.allow_cheats"));
Tag tag = dataTag.get("allowCommands");
if (tag instanceof ByteTag) {
ByteTag byteTag = (ByteTag) tag;
byte value = byteTag.getValue();
if (value == 0 || value == 1) {
allowCheatsButton.setSelected(value == 1);
allowCheatsButton.selectedProperty().addListener((o, oldValue, newValue) -> {
byteTag.setValue(newValue ? (byte) 1 : (byte) 0);
saveLevelDat();
});
} else {
allowCheatsButton.setDisable(true);
}
} else {
allowCheatsButton.setDisable(true);
}
}
OptionToggleButton generateFeaturesButton = new OptionToggleButton();
{
generateFeaturesButton.setTitle(i18n("world.info.generate_features"));
Tag tag = worldGenSettings != null ? worldGenSettings.get("generate_features") : dataTag.get("MapFeatures");
if (tag instanceof ByteTag) {
ByteTag byteTag = (ByteTag) tag;
byte value = byteTag.getValue();
if (value == 0 || value == 1) {
generateFeaturesButton.setSelected(value == 1);
generateFeaturesButton.selectedProperty().addListener((o, oldValue, newValue) -> {
byteTag.setValue(newValue ? (byte) 1 : (byte) 0);
saveLevelDat();
});
} else {
generateFeaturesButton.setDisable(true);
}
} else {
generateFeaturesButton.setDisable(true);
}
}
BorderPane difficultyPane = new BorderPane();
{
Label label = new Label(i18n("world.info.difficulty"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
difficultyPane.setLeft(label);
JFXComboBox<Difficulty> difficultyBox = new JFXComboBox<>(Difficulty.items);
BorderPane.setAlignment(difficultyBox, Pos.CENTER_RIGHT);
difficultyPane.setRight(difficultyBox);
Tag tag = dataTag.get("Difficulty");
if (tag instanceof ByteTag) {
ByteTag byteTag = (ByteTag) tag;
Difficulty difficulty = Difficulty.of(byteTag.getValue());
if (difficulty != null) {
difficultyBox.setValue(difficulty);
difficultyBox.valueProperty().addListener((o, oldValue, newValue) -> {
if (newValue != null) {
byteTag.setValue((byte) newValue.ordinal());
saveLevelDat();
}
});
} else {
difficultyBox.setDisable(true);
}
} else {
difficultyBox.setDisable(true);
}
}
basicInfo.getContent().setAll(
worldNamePane, gameVersionPane, randomSeedPane, lastPlayedPane, timePane,
allowCheatsButton, generateFeaturesButton, difficultyPane);
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info.basic")), basicInfo);
}
Tag playerTag = dataTag.get("Player");
if (playerTag instanceof CompoundTag) {
CompoundTag player = (CompoundTag) playerTag;
ComponentList playerInfo = new ComponentList();
BorderPane locationPane = new BorderPane();
{
Label label = new Label(i18n("world.info.player.location"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
locationPane.setLeft(label);
Label locationLabel = new Label();
BorderPane.setAlignment(locationLabel, Pos.CENTER_RIGHT);
locationPane.setRight(locationLabel);
Dimension dim = Dimension.of(player.get("Dimension"));
if (dim != null) {
String posString = dim.formatPosition(player.get("Pos"));
if (posString != null)
locationLabel.setText(posString);
}
}
BorderPane lastDeathLocationPane = new BorderPane();
{
Label label = new Label(i18n("world.info.player.last_death_location"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
lastDeathLocationPane.setLeft(label);
Label lastDeathLocationLabel = new Label();
BorderPane.setAlignment(lastDeathLocationLabel, Pos.CENTER_RIGHT);
lastDeathLocationPane.setRight(lastDeathLocationLabel);
Tag tag = player.get("LastDeathLocation");
if (tag instanceof CompoundTag) {
Dimension dim = Dimension.of(((CompoundTag) tag).get("dimension"));
if (dim != null) {
String posString = dim.formatPosition(((CompoundTag) tag).get("pos"));
if (posString != null)
lastDeathLocationLabel.setText(posString);
}
}
}
BorderPane spawnPane = new BorderPane();
{
Label label = new Label(i18n("world.info.player.spawn"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
spawnPane.setLeft(label);
Label spawnLabel = new Label();
BorderPane.setAlignment(spawnLabel, Pos.CENTER_RIGHT);
spawnPane.setRight(spawnLabel);
Dimension dim = Dimension.of(player.get("SpawnDimension"));
if (dim != null) {
Tag x = player.get("SpawnX");
Tag y = player.get("SpawnY");
Tag z = player.get("SpawnZ");
if (x instanceof IntTag && y instanceof IntTag && z instanceof IntTag)
spawnLabel.setText(dim.formatPosition(((IntTag) x).getValue(), ((IntTag) y).getValue(), ((IntTag) z).getValue()));
}
}
BorderPane playerGameTypePane = new BorderPane();
{
Label label = new Label(i18n("world.info.player.game_type"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
playerGameTypePane.setLeft(label);
JFXComboBox<GameType> gameTypeBox = new JFXComboBox<>(GameType.items);
BorderPane.setAlignment(gameTypeBox, Pos.CENTER_RIGHT);
playerGameTypePane.setRight(gameTypeBox);
Tag tag = player.get("playerGameType");
if (tag instanceof IntTag) {
IntTag intTag = (IntTag) tag;
GameType gameType = GameType.of(intTag.getValue());
if (gameType != null) {
gameTypeBox.setValue(gameType);
gameTypeBox.valueProperty().addListener((o, oldValue, newValue) -> {
if (newValue != null) {
intTag.setValue(newValue.ordinal());
saveLevelDat();
}
});
} else {
gameTypeBox.setDisable(true);
}
} else {
gameTypeBox.setDisable(true);
}
}
BorderPane healthPane = new BorderPane();
{
Label label = new Label(i18n("world.info.player.health"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
healthPane.setLeft(label);
JFXTextField healthField = new JFXTextField();
healthField.setPrefWidth(50);
healthField.setAlignment(Pos.CENTER_RIGHT);
BorderPane.setAlignment(healthField, Pos.CENTER_RIGHT);
healthPane.setRight(healthField);
Tag tag = player.get("Health");
if (tag instanceof FloatTag) {
FloatTag floatTag = (FloatTag) tag;
healthField.setText(new DecimalFormat("#").format(floatTag.getValue().floatValue()));
healthField.textProperty().addListener((o, oldValue, newValue) -> {
if (newValue != null) {
try {
floatTag.setValue(Float.parseFloat(newValue));
saveLevelDat();
} catch (Throwable ignored) {
}
}
});
FXUtils.setValidateWhileTextChanged(healthField, true);
healthField.setValidators(new DoubleValidator(i18n("input.number"), true));
} else {
healthField.setDisable(true);
}
}
BorderPane foodLevelPane = new BorderPane();
{
Label label = new Label(i18n("world.info.player.food_level"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
foodLevelPane.setLeft(label);
JFXTextField foodLevelField = new JFXTextField();
foodLevelField.setPrefWidth(50);
foodLevelField.setAlignment(Pos.CENTER_RIGHT);
BorderPane.setAlignment(foodLevelField, Pos.CENTER_RIGHT);
foodLevelPane.setRight(foodLevelField);
Tag tag = player.get("foodLevel");
if (tag instanceof IntTag) {
IntTag intTag = (IntTag) tag;
foodLevelField.setText(String.valueOf(intTag.getValue()));
foodLevelField.textProperty().addListener((o, oldValue, newValue) -> {
if (newValue != null) {
try {
intTag.setValue(Integer.parseInt(newValue));
saveLevelDat();
} catch (Throwable ignored) {
}
}
});
FXUtils.setValidateWhileTextChanged(foodLevelField, true);
foodLevelField.setValidators(new NumberValidator(i18n("input.number"), true));
} else {
foodLevelField.setDisable(true);
}
}
BorderPane xpLevelPane = new BorderPane();
{
Label label = new Label(i18n("world.info.player.xp_level"));
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
xpLevelPane.setLeft(label);
JFXTextField xpLevelField = new JFXTextField();
xpLevelField.setPrefWidth(50);
xpLevelField.setAlignment(Pos.CENTER_RIGHT);
BorderPane.setAlignment(xpLevelField, Pos.CENTER_RIGHT);
xpLevelPane.setRight(xpLevelField);
Tag tag = player.get("XpLevel");
if (tag instanceof IntTag) {
IntTag intTag = (IntTag) tag;
xpLevelField.setText(String.valueOf(intTag.getValue()));
xpLevelField.textProperty().addListener((o, oldValue, newValue) -> {
if (newValue != null) {
try {
intTag.setValue(Integer.parseInt(newValue));
saveLevelDat();
} catch (Throwable ignored) {
}
}
});
FXUtils.setValidateWhileTextChanged(xpLevelField, true);
xpLevelField.setValidators(new NumberValidator(i18n("input.number"), true));
} else {
xpLevelField.setDisable(true);
}
}
playerInfo.getContent().setAll(
locationPane, lastDeathLocationPane, spawnPane,
playerGameTypePane, healthPane, foodLevelPane, xpLevelPane
);
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("world.info.player")), playerInfo);
}
}
private void saveLevelDat() {
LOG.info("Saving level.dat of world " + world.getWorldName());
try {
this.world.writeLevelDat(levelDat);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to save level.dat of world " + world.getWorldName(), e);
}
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return stateProperty;
}
private static final class Dimension {
static final Dimension OVERWORLD = new Dimension(null);
static final Dimension THE_NETHER = new Dimension(i18n("world.info.dimension.the_nether"));
static final Dimension THE_END = new Dimension(i18n("world.info.dimension.the_end"));
final String name;
static Dimension of(Tag tag) {
if (tag instanceof IntTag) {
switch (((IntTag) tag).getValue()) {
case 0:
return OVERWORLD;
case 1:
return THE_NETHER;
case 2:
return THE_END;
default:
return null;
}
} else if (tag instanceof StringTag) {
String id = ((StringTag) tag).getValue();
switch (id) {
case "overworld":
case "minecraft:overworld":
return OVERWORLD;
case "the_nether":
case "minecraft:the_nether":
return THE_NETHER;
case "the_end":
case "minecraft:the_end":
return THE_END;
default:
return new Dimension(id);
}
} else {
return null;
}
}
private Dimension(String name) {
this.name = name;
}
String formatPosition(Tag tag) {
if (tag instanceof ListTag) {
ListTag listTag = (ListTag) tag;
if (listTag.size() != 3)
return null;
Tag x = listTag.get(0);
Tag y = listTag.get(1);
Tag z = listTag.get(2);
if (x instanceof DoubleTag && y instanceof DoubleTag && z instanceof DoubleTag) {
//noinspection MalformedFormatString
return this == OVERWORLD
? String.format("(%.2f, %.2f, %.2f)", x.getValue(), y.getValue(), z.getValue())
: String.format("%s (%.2f, %.2f, %.2f)", name, x.getValue(), y.getValue(), z.getValue());
}
return null;
}
if (tag instanceof IntArrayTag) {
IntArrayTag intArrayTag = (IntArrayTag) tag;
int x = intArrayTag.getValue(0);
int y = intArrayTag.getValue(1);
int z = intArrayTag.getValue(2);
return this == OVERWORLD
? String.format("(%d, %d, %d)", x, y, z)
: String.format("%s (%d, %d, %d)", name, x, y, z);
}
return null;
}
String formatPosition(int x, int y, int z) {
return this == OVERWORLD
? String.format("(%d, %d, %d)", x, y, z)
: String.format("%s (%d, %d, %d)", name, x, y, z);
}
String formatPosition(double x, double y, double z) {
return this == OVERWORLD
? String.format("(%.2f, %.2f, %.2f)", x, y, z)
: String.format("%s (%.2f, %.2f, %.2f)", name, x, y, z);
}
}
private enum Difficulty {
PEACEFUL, EASY, NORMAL, HARD;
static final ObservableList<Difficulty> items = FXCollections.observableList(Arrays.asList(values()));
static Difficulty of(int d) {
return d >= 0 && d <= items.size() ? items.get(d) : null;
}
@Override
public String toString() {
return i18n("world.info.difficulty." + name().toLowerCase(Locale.ROOT));
}
}
private enum GameType {
SURVIVAL, CREATIVE, ADVENTURE, SPECTATOR;
static final ObservableList<GameType> items = FXCollections.observableList(Arrays.asList(values()));
static GameType of(int d) {
return d >= 0 && d <= items.size() ? items.get(d) : null;
}
@Override
public String toString() {
return i18n("world.info.player.game_type." + name().toLowerCase(Locale.ROOT));
}
}
}

View File

@ -50,6 +50,8 @@ public class WorldListItem extends Control {
title.set(parseColorEscapes(world.getWorldName())); title.set(parseColorEscapes(world.getWorldName()));
subtitle.set(i18n("world.description", world.getFileName(), Locales.SIMPLE_DATE_FORMAT.get().format(new Date(world.getLastPlayed())), world.getGameVersion() == null ? i18n("message.unknown") : world.getGameVersion())); subtitle.set(i18n("world.description", world.getFileName(), Locales.SIMPLE_DATE_FORMAT.get().format(new Date(world.getLastPlayed())), world.getGameVersion() == null ? i18n("message.unknown") : world.getGameVersion()));
setOnMouseClicked(event -> showInfo());
} }
@Override @Override
@ -99,4 +101,12 @@ public class WorldListItem extends Control {
} }
Controllers.navigate(new DatapackListPage(world.getWorldName(), world.getFile())); Controllers.navigate(new DatapackListPage(world.getWorldName(), world.getFile()));
} }
public void showInfo() {
try {
Controllers.navigate(new WorldInfoPage(world));
} catch (Exception e) {
// TODO
}
}
} }

View File

@ -937,6 +937,34 @@ world.import.already_exists=This world already exists.
world.import.choose=Select the save archive you want to import world.import.choose=Select the save archive you want to import
world.import.failed=Unable to import this world\: %s world.import.failed=Unable to import this world\: %s
world.import.invalid=Unable to parse the save. world.import.invalid=Unable to parse the save.
world.info.title=World %s - Information
world.info.basic=Basic Information
world.info.allow_cheats=Allow Cheats
world.info.dimension.the_nether=The Nether
world.info.dimension.the_end=The End
world.info.difficulty=Difficulty
world.info.difficulty.peaceful=Peaceful
world.info.difficulty.easy=Easy
world.info.difficulty.normal=Normal
world.info.difficulty.hard=Hard
world.info.game_version=Game Version
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.game_type=Game Mode
world.info.player.game_type.adventure=Adventure
world.info.player.game_type.creative=Creative
world.info.player.game_type.spectator=Spectator
world.info.player.game_type.survival=Survival
world.info.player.health=Health
world.info.player.last_death_location=Last Death Location
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.manage=Worlds / Datapacks world.manage=Worlds / Datapacks
world.name=World Name world.name=World Name
world.name.enter=Enter the world name world.name.enter=Enter the world name

View File

@ -777,6 +777,34 @@ world.import.already_exists=此世界已經存在
world.import.choose=選擇要匯入的存檔壓縮檔 world.import.choose=選擇要匯入的存檔壓縮檔
world.import.failed=無法匯入此世界: %s world.import.failed=無法匯入此世界: %s
world.import.invalid=無法識別的存檔壓縮包 world.import.invalid=無法識別的存檔壓縮包
world.info.title=世界 %s - 世界資訊
world.info.basic=基本資訊
world.info.allow_cheats=允許作弊
world.info.dimension.the_nether=下界
world.info.dimension.the_end=末地
world.info.difficulty=難度
world.info.difficulty.peaceful=和平
world.info.difficulty.easy=簡單
world.info.difficulty.normal=普通
world.info.difficulty.hard=困難
world.info.game_version=遊戲版本
world.info.last_played=上一次遊戲時間
world.info.generate_features=生成建築
world.info.player=玩家資訊
world.info.player.food_level=饑餓值
world.info.player.game_type=遊戲模式
world.info.player.game_type.adventure=極限
world.info.player.game_type.creative=創造
world.info.player.game_type.spectator=旁觀
world.info.player.game_type.survival=生存
world.info.player.health=生命值
world.info.player.last_death_location=上次死亡位置
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.game_version=遊戲版本 world.game_version=遊戲版本
world.manage=世界/資料包 world.manage=世界/資料包
world.name=世界名稱 world.name=世界名稱

View File

@ -778,6 +778,34 @@ world.import.already_exists=此世界已经存在
world.import.choose=选择要导入的存档压缩包 world.import.choose=选择要导入的存档压缩包
world.import.failed=无法导入此世界:%s world.import.failed=无法导入此世界:%s
world.import.invalid=无法识别该存档压缩包 world.import.invalid=无法识别该存档压缩包
world.info.title=世界 %s - 世界信息
world.info.basic=基本信息
world.info.allow_cheats=允许作弊
world.info.dimension.the_nether=下界
world.info.dimension.the_end=末地
world.info.difficulty=难度
world.info.difficulty.peaceful=和平
world.info.difficulty.easy=简单
world.info.difficulty.normal=普通
world.info.difficulty.hard=困难
world.info.game_version=游戏版本
world.info.last_played=上一次游戏时间
world.info.generate_features=生成建筑
world.info.player=玩家信息
world.info.player.food_level=饥饿值
world.info.player.game_type=游戏模式
world.info.player.game_type.adventure=极限
world.info.player.game_type.creative=创造
world.info.player.game_type.spectator=旁观
world.info.player.game_type.survival=生存
world.info.player.health=生命值
world.info.player.last_death_location=上次死亡位置
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.manage=世界/数据包 world.manage=世界/数据包
world.name=世界名称 world.name=世界名称
world.name.enter=输入世界名称 world.name.enter=输入世界名称

View File

@ -28,7 +28,6 @@ import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.Unzipper; import org.jackhuang.hmcl.util.io.Unzipper;
import org.jackhuang.hmcl.util.io.Zipper; import org.jackhuang.hmcl.util.io.Zipper;
import java.io.BufferedInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
@ -77,6 +76,10 @@ public class World {
return worldName; return worldName;
} }
public Path getLevelDatFile() {
return file.resolve("level.dat");
}
public long getLastPlayed() { public long getLastPlayed() {
return lastPlayed; return lastPlayed;
} }
@ -142,14 +145,10 @@ public class World {
throw new IOException("Not a valid world directory"); throw new IOException("Not a valid world directory");
// Change the name recorded in level.dat // Change the name recorded in level.dat
Path levelDat = file.resolve("level.dat"); CompoundTag nbt = readLevelDat();
CompoundTag nbt = parseLevelDat(levelDat);
CompoundTag data = nbt.get("Data"); CompoundTag data = nbt.get("Data");
data.put(new StringTag("LevelName", newName)); data.put(new StringTag("LevelName", newName));
writeLevelDat(nbt);
try (OutputStream os = new GZIPOutputStream(Files.newOutputStream(levelDat))) {
NBTIO.writeTag(os, nbt);
}
// then change the folder's name // then change the folder's name
Files.move(file, file.resolveSibling(newName)); Files.move(file, file.resolveSibling(newName));
@ -203,8 +202,26 @@ public class World {
} }
} }
public CompoundTag readLevelDat() throws IOException {
if (!Files.isDirectory(file))
throw new IOException("Not a valid world directory");
return parseLevelDat(getLevelDatFile());
}
public void writeLevelDat(CompoundTag nbt) throws IOException {
if (!Files.isDirectory(file))
throw new IOException("Not a valid world directory");
FileUtils.saveSafely(getLevelDatFile(), os -> {
try (OutputStream gos = new GZIPOutputStream(os)) {
NBTIO.writeTag(gos, nbt);
}
});
}
private static CompoundTag parseLevelDat(Path path) throws IOException { private static CompoundTag parseLevelDat(Path path) throws IOException {
try (InputStream is = new BufferedInputStream(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)
return (CompoundTag) nbt; return (CompoundTag) nbt;

View File

@ -235,6 +235,15 @@ public final class Lang {
} }
} }
public static Double toDoubleOrNull(Object string) {
try {
if (string == null) return null;
return Double.parseDouble(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.
* @param t nullable references list. * @param t nullable references list.

View File

@ -19,11 +19,9 @@ package org.jackhuang.hmcl.util.io;
import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.function.ExceptionalConsumer;
import java.io.BufferedWriter; import java.io.*;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.file.*; import java.nio.file.*;
@ -445,4 +443,21 @@ public final class FileUtils {
Files.move(tmpFile, file, StandardCopyOption.REPLACE_EXISTING); Files.move(tmpFile, file, StandardCopyOption.REPLACE_EXISTING);
} }
public static void saveSafely(Path file, ExceptionalConsumer<? super OutputStream, IOException> action) throws IOException {
Path tmpFile = tmpSaveFile(file);
try (OutputStream os = Files.newOutputStream(tmpFile, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE)) {
action.accept(os);
}
try {
if (Files.exists(file) && Files.getAttribute(file, "dos:hidden") == Boolean.TRUE) {
Files.setAttribute(tmpFile, "dos:hidden", true);
}
} catch (Throwable ignored) {
}
Files.move(tmpFile, file, StandardCopyOption.REPLACE_EXISTING);
}
} }