支持查看 NBT 文件 (#2402)
* init * fix checkstyle * update RootPage * fix build * update * update
@@ -39,9 +39,12 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
|
||||
import org.jackhuang.hmcl.ui.nbt.NBTEditorPage;
|
||||
import org.jackhuang.hmcl.ui.nbt.NBTHelper;
|
||||
import org.jackhuang.hmcl.ui.versions.GameAdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
import org.jackhuang.hmcl.upgrade.hmcl.UpdateChecker;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
@@ -51,10 +54,12 @@ import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
@@ -85,12 +90,24 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
public MainPage getMainPage() {
|
||||
if (mainPage == null) {
|
||||
MainPage mainPage = new MainPage();
|
||||
FXUtils.applyDragListener(mainPage, ModpackHelper::isFileModpackByExtension, modpacks -> {
|
||||
File modpack = modpacks.get(0);
|
||||
Controllers.getDecorator().startWizard(
|
||||
new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack),
|
||||
i18n("install.modpack"));
|
||||
});
|
||||
FXUtils.applyDragListener(mainPage,
|
||||
file -> ModpackHelper.isFileModpackByExtension(file) || NBTHelper.isNBTFileByExtension(file),
|
||||
modpacks -> {
|
||||
File file = modpacks.get(0);
|
||||
if (ModpackHelper.isFileModpackByExtension(file)) {
|
||||
Controllers.getDecorator().startWizard(
|
||||
new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), file),
|
||||
i18n("install.modpack"));
|
||||
} else if (NBTHelper.isNBTFileByExtension(file)) {
|
||||
try {
|
||||
Controllers.navigate(new NBTEditorPage(file));
|
||||
} catch (Throwable e) {
|
||||
LOG.log(Level.WARNING, "Fail to open nbt file", e);
|
||||
Controllers.dialog(i18n("nbt.open.failed") + "\n\n" + StringUtils.getStackTrace(e),
|
||||
i18n("message.error"), MessageDialogPane.MessageType.ERROR);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), mainPage::setCurrentGame);
|
||||
mainPage.showUpdateProperty().bind(UpdateChecker.outdatedProperty());
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package org.jackhuang.hmcl.ui.nbt;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class NBTEditorPage extends BorderPane implements DecoratorPage {
|
||||
private final ReadOnlyObjectWrapper<State> state;
|
||||
private final File file;
|
||||
private final NBTFileType type;
|
||||
|
||||
public NBTEditorPage(File file) throws IOException {
|
||||
getStyleClass().add("gray-background");
|
||||
|
||||
this.state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("nbt.title", file.getAbsolutePath())));
|
||||
this.file = file;
|
||||
this.type = NBTFileType.ofFile(file);
|
||||
|
||||
if (type == null) {
|
||||
throw new IOException("Unknown type of file " + file);
|
||||
}
|
||||
|
||||
setCenter(new ProgressIndicator());
|
||||
|
||||
HBox actions = new HBox(8);
|
||||
actions.setPadding(new Insets(8));
|
||||
actions.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
JFXButton saveButton = new JFXButton(i18n("button.save"));
|
||||
saveButton.getStyleClass().add("jfx-button-raised");
|
||||
saveButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
saveButton.setOnAction(e -> {
|
||||
try {
|
||||
save();
|
||||
} catch (IOException ex) {
|
||||
LOG.log(Level.WARNING, "Failed to save NBT file", ex);
|
||||
Controllers.dialog(i18n("nbt.save.failed") + "\n\n" + StringUtils.getStackTrace(ex));
|
||||
}
|
||||
});
|
||||
|
||||
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
|
||||
cancelButton.getStyleClass().add("jfx-button-raised");
|
||||
cancelButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
cancelButton.setOnAction(e -> fireEvent(new PageCloseEvent()));
|
||||
onEscPressed(this, cancelButton::fire);
|
||||
|
||||
actions.getChildren().setAll(saveButton, cancelButton);
|
||||
|
||||
CompletableFuture.supplyAsync(Lang.wrap(() -> type.readAsTree(file)))
|
||||
.thenAcceptAsync(tree -> {
|
||||
setCenter(new NBTTreeView(tree));
|
||||
// setBottom(actions);
|
||||
}, Schedulers.javafx())
|
||||
.handleAsync((result, e) -> {
|
||||
if (e != null) {
|
||||
LOG.log(Level.WARNING, "Fail to open nbt file", e);
|
||||
Controllers.dialog(i18n("nbt.open.failed") + "\n\n" + StringUtils.getStackTrace(e), null, MessageDialogPane.MessageType.WARNING, cancelButton::fire);
|
||||
}
|
||||
return null;
|
||||
}, Schedulers.javafx());
|
||||
}
|
||||
|
||||
public void save() throws IOException {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyObjectProperty<State> stateProperty() {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
148
HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTFileType.java
Normal file
@@ -0,0 +1,148 @@
|
||||
package org.jackhuang.hmcl.ui.nbt;
|
||||
|
||||
import com.github.steveice10.opennbt.NBTIO;
|
||||
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.IntTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.ListTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.Tag;
|
||||
import org.apache.commons.compress.utils.BoundedInputStream;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.Inflater;
|
||||
import java.util.zip.InflaterInputStream;
|
||||
|
||||
public enum NBTFileType {
|
||||
COMPRESSED("dat", "dat_old") {
|
||||
@Override
|
||||
public Tag read(File file) throws IOException {
|
||||
try (InputStream in = new GZIPInputStream(new FileInputStream(file))) {
|
||||
Tag tag = NBTIO.readTag(in);
|
||||
if (!(tag instanceof CompoundTag))
|
||||
throw new IOException("Unexpected tag: " + tag);
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
},
|
||||
ANVIL("mca") {
|
||||
@Override
|
||||
public Tag read(File file) throws IOException {
|
||||
return REGION.read(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NBTTreeView.Item readAsTree(File file) throws IOException {
|
||||
return REGION.readAsTree(file);
|
||||
}
|
||||
},
|
||||
REGION("mcr") {
|
||||
@Override
|
||||
public Tag read(File file) throws IOException {
|
||||
try (RandomAccessFile r = new RandomAccessFile(file, "r")) {
|
||||
byte[] header = new byte[4096];
|
||||
byte[] buffer = new byte[1 * 1024 * 1024]; // The maximum size of each chunk is 1MiB
|
||||
Inflater inflater = new Inflater();
|
||||
|
||||
ListTag tag = new ListTag(file.getName(), CompoundTag.class);
|
||||
|
||||
r.readFully(header);
|
||||
for (int i = 0; i < 4096; i += 4) {
|
||||
int offset = ((header[i] & 0xff) << 16) + ((header[i + 1] & 0xff) << 8) + (header[i + 2] & 0xff);
|
||||
int length = header[i + 3] & 0xff;
|
||||
|
||||
if (offset == 0 || length == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
r.seek(offset * 4096L);
|
||||
r.readFully(buffer, 0, length * 4096);
|
||||
|
||||
int chunkLength = ((buffer[0] & 0xff) << 24) + ((buffer[1] & 0xff) << 16) + ((buffer[2] & 0xff) << 8) + (buffer[3] & 0xff);
|
||||
|
||||
InputStream input = new ByteArrayInputStream(buffer);
|
||||
input.skip(5);
|
||||
input = new BoundedInputStream(input, chunkLength - 1);
|
||||
|
||||
switch (buffer[4]) {
|
||||
case 0x01:
|
||||
// GZip
|
||||
input = new GZIPInputStream(input);
|
||||
break;
|
||||
case 0x02:
|
||||
// Zlib
|
||||
inflater.reset();
|
||||
input = new InflaterInputStream(input, inflater);
|
||||
break;
|
||||
case 0x03:
|
||||
// Uncompressed
|
||||
break;
|
||||
default:
|
||||
throw new IOException("Unsupported compression method: " + Integer.toHexString(buffer[4] & 0xff));
|
||||
}
|
||||
|
||||
try (InputStream in = input) {
|
||||
Tag chunk = NBTIO.readTag(in);
|
||||
if (!(chunk instanceof CompoundTag))
|
||||
throw new IOException("Unexpected tag: " + chunk);
|
||||
|
||||
tag.add(chunk);
|
||||
}
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public NBTTreeView.Item readAsTree(File file) throws IOException {
|
||||
NBTTreeView.Item item = new NBTTreeView.Item(read(file));
|
||||
|
||||
for (Tag tag : ((ListTag) item.getValue())) {
|
||||
CompoundTag chunk = (CompoundTag) tag;
|
||||
|
||||
NBTTreeView.Item tree = NBTTreeView.buildTree(chunk);
|
||||
|
||||
Tag xPos = chunk.get("xPos");
|
||||
Tag zPos = chunk.get("zPos");
|
||||
|
||||
if (xPos instanceof IntTag && zPos instanceof IntTag) {
|
||||
tree.setText(String.format("Chunk: %d %d", xPos.getValue(), zPos.getValue()));
|
||||
} else {
|
||||
tree.setText("Chunk: Unknown");
|
||||
}
|
||||
|
||||
item.getChildren().add(tree);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
};
|
||||
|
||||
static final NBTFileType[] types = values();
|
||||
|
||||
public static NBTFileType ofFile(File file) {
|
||||
String ext = FileUtils.getExtension(file);
|
||||
for (NBTFileType type : types) {
|
||||
for (String extension : type.extensions) {
|
||||
if (extension.equals(ext))
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private final String[] extensions;
|
||||
|
||||
NBTFileType(String... extensions) {
|
||||
this.extensions = extensions;
|
||||
}
|
||||
|
||||
public abstract Tag read(File file) throws IOException;
|
||||
|
||||
public NBTTreeView.Item readAsTree(File file) throws IOException {
|
||||
NBTTreeView.Item root = NBTTreeView.buildTree(read(file));
|
||||
root.setName(file.getName());
|
||||
return root;
|
||||
}
|
||||
}
|
||||
12
HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTHelper.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package org.jackhuang.hmcl.ui.nbt;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public final class NBTHelper {
|
||||
private NBTHelper() {
|
||||
}
|
||||
|
||||
public static boolean isNBTFileByExtension(File file) {
|
||||
return NBTFileType.ofFile(file) != null;
|
||||
}
|
||||
}
|
||||
59
HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTTagType.java
Normal file
@@ -0,0 +1,59 @@
|
||||
package org.jackhuang.hmcl.ui.nbt;
|
||||
|
||||
import com.github.steveice10.opennbt.tag.builtin.Tag;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
public enum NBTTagType {
|
||||
BYTE, SHORT, INT, LONG, FLOAT, DOUBLE,
|
||||
BYTE_ARRAY, INT_ARRAY, LONG_ARRAY,
|
||||
STRING,
|
||||
LIST, COMPOUND;
|
||||
|
||||
private static final Map<String, NBTTagType> lookupTable = new HashMap<>();
|
||||
|
||||
static {
|
||||
for (NBTTagType type : values()) {
|
||||
lookupTable.put(type.getTagClassName(), type);
|
||||
}
|
||||
}
|
||||
|
||||
public static NBTTagType typeOf(Tag tag) {
|
||||
NBTTagType type = lookupTable.get(tag.getClass().getSimpleName());
|
||||
if (type == null) {
|
||||
throw new IllegalArgumentException("Unknown tag: " + type);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
private final String iconUrl;
|
||||
private final String tagClassName;
|
||||
|
||||
NBTTagType() {
|
||||
String tagName;
|
||||
String className;
|
||||
|
||||
int idx = name().indexOf('_');
|
||||
if (idx < 0) {
|
||||
tagName = name().charAt(0) + name().substring(1).toLowerCase(Locale.ROOT);
|
||||
className = tagName + "Tag";
|
||||
} else {
|
||||
tagName = name().charAt(0) + name().substring(1, idx + 1).toLowerCase(Locale.ROOT)
|
||||
+ name().charAt(idx + 1) + name().substring(idx + 2).toLowerCase(Locale.ROOT);
|
||||
className = tagName.substring(0, idx) + tagName.substring(idx + 1) + "Tag";
|
||||
}
|
||||
|
||||
this.iconUrl = "/assets/img/nbt/TAG_" + tagName + ".png";
|
||||
this.tagClassName = className;
|
||||
}
|
||||
|
||||
public String getIconUrl() {
|
||||
return iconUrl;
|
||||
}
|
||||
|
||||
public String getTagClassName() {
|
||||
return tagClassName;
|
||||
}
|
||||
}
|
||||
152
HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTTreeView.java
Normal file
@@ -0,0 +1,152 @@
|
||||
package org.jackhuang.hmcl.ui.nbt;
|
||||
|
||||
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.ListTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.Tag;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.util.Callback;
|
||||
import org.jackhuang.hmcl.util.Holder;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.EnumMap;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class NBTTreeView extends TreeView<Tag> {
|
||||
|
||||
public NBTTreeView(NBTTreeView.Item tree) {
|
||||
this.setRoot(tree);
|
||||
this.setCellFactory(cellFactory());
|
||||
}
|
||||
|
||||
private static Callback<TreeView<Tag>, TreeCell<Tag>> cellFactory() {
|
||||
Holder<Object> lastCell = new Holder<>();
|
||||
EnumMap<NBTTagType, Image> icons = new EnumMap<>(NBTTagType.class);
|
||||
|
||||
return view -> new TreeCell<Tag>() {
|
||||
private void setTagText(String text) {
|
||||
String name = ((Item) getTreeItem()).getName();
|
||||
|
||||
if (name == null) {
|
||||
setText(text);
|
||||
} else if (text == null) {
|
||||
setText(name);
|
||||
} else {
|
||||
setText(name + ": " + text);
|
||||
}
|
||||
}
|
||||
|
||||
private void setTagText(int nEntries) {
|
||||
setTagText(i18n("nbt.entries", nEntries));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateItem(Tag item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
|
||||
if (this == lastCell.value && !isVisible())
|
||||
return;
|
||||
lastCell.value = this;
|
||||
|
||||
ImageView imageView = (ImageView) this.getGraphic();
|
||||
if (imageView == null) {
|
||||
imageView = new ImageView();
|
||||
this.setGraphic(imageView);
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
imageView.setImage(null);
|
||||
setText(null);
|
||||
return;
|
||||
}
|
||||
|
||||
NBTTagType tagType = NBTTagType.typeOf(item);
|
||||
imageView.setImage(icons.computeIfAbsent(tagType, type -> new Image(type.getIconUrl())));
|
||||
|
||||
if (((Item) getTreeItem()).getText() != null) {
|
||||
setText(((Item) getTreeItem()).getText());
|
||||
} else {
|
||||
switch (tagType) {
|
||||
case BYTE:
|
||||
case SHORT:
|
||||
case INT:
|
||||
case LONG:
|
||||
case FLOAT:
|
||||
case DOUBLE:
|
||||
case STRING:
|
||||
setTagText(item.getValue().toString());
|
||||
break;
|
||||
case BYTE_ARRAY:
|
||||
case INT_ARRAY:
|
||||
case LONG_ARRAY:
|
||||
setTagText(Array.getLength(item.getValue()));
|
||||
break;
|
||||
case LIST:
|
||||
setTagText(((ListTag) item).size());
|
||||
break;
|
||||
case COMPOUND:
|
||||
setTagText(((CompoundTag) item).size());
|
||||
break;
|
||||
default:
|
||||
setTagText(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static Item buildTree(Tag tag) {
|
||||
Item item = new Item(tag);
|
||||
|
||||
if (tag instanceof CompoundTag) {
|
||||
for (Tag subTag : ((CompoundTag) tag)) {
|
||||
item.getChildren().add(buildTree(subTag));
|
||||
}
|
||||
} else if (tag instanceof ListTag) {
|
||||
int idx = 0;
|
||||
for (Tag subTag : ((ListTag) tag)) {
|
||||
Item subTree = buildTree(subTag);
|
||||
subTree.setName(String.valueOf(idx++));
|
||||
item.getChildren().add(subTree);
|
||||
}
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
public CompoundTag getRootTag() {
|
||||
return ((CompoundTag) getRoot().getValue());
|
||||
}
|
||||
|
||||
public static class Item extends TreeItem<Tag> {
|
||||
|
||||
private String text;
|
||||
private String name;
|
||||
|
||||
public Item() {
|
||||
}
|
||||
|
||||
public Item(Tag value) {
|
||||
super(value);
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name == null ? getValue().getName() : name;
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
HMCL/src/main/resources/assets/img/nbt/TAG_Byte.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
HMCL/src/main/resources/assets/img/nbt/TAG_Byte@2x.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
HMCL/src/main/resources/assets/img/nbt/TAG_Byte_Array.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
HMCL/src/main/resources/assets/img/nbt/TAG_Byte_Array@2x.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
HMCL/src/main/resources/assets/img/nbt/TAG_Compound.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
HMCL/src/main/resources/assets/img/nbt/TAG_Compound@2x.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
HMCL/src/main/resources/assets/img/nbt/TAG_Double.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
HMCL/src/main/resources/assets/img/nbt/TAG_Double@2x.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
HMCL/src/main/resources/assets/img/nbt/TAG_Float.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
HMCL/src/main/resources/assets/img/nbt/TAG_Float@2x.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
HMCL/src/main/resources/assets/img/nbt/TAG_Int.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
HMCL/src/main/resources/assets/img/nbt/TAG_Int@2x.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
HMCL/src/main/resources/assets/img/nbt/TAG_Int_Array.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
HMCL/src/main/resources/assets/img/nbt/TAG_Int_Array@2x.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
HMCL/src/main/resources/assets/img/nbt/TAG_List.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
HMCL/src/main/resources/assets/img/nbt/TAG_List@2x.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
HMCL/src/main/resources/assets/img/nbt/TAG_Long.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
HMCL/src/main/resources/assets/img/nbt/TAG_Long@2x.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
HMCL/src/main/resources/assets/img/nbt/TAG_Long_Array.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
HMCL/src/main/resources/assets/img/nbt/TAG_Long_Array@2x.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
HMCL/src/main/resources/assets/img/nbt/TAG_Short.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
HMCL/src/main/resources/assets/img/nbt/TAG_Short@2x.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
HMCL/src/main/resources/assets/img/nbt/TAG_String.png
Normal file
|
After Width: | Height: | Size: 980 B |
BIN
HMCL/src/main/resources/assets/img/nbt/TAG_String@2x.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
@@ -906,6 +906,11 @@ multiplayer=Multiplayer online
|
||||
multiplayer.hint=Multiplayer online function is under maintenance.
|
||||
multiplayer.hint.details=View details
|
||||
|
||||
nbt.entries=%s entries
|
||||
nbt.open.failed=Fail to open file
|
||||
nbt.save.failed=Fail to save file
|
||||
nbt.title=View File - %s
|
||||
|
||||
datapack=Datapacks
|
||||
datapack.add=Install datapack
|
||||
datapack.choose_datapack=Select a datapack to import
|
||||
|
||||
@@ -780,6 +780,11 @@ multiplayer=多人聯機
|
||||
multiplayer.hint=多人聯機服務正在維護中。
|
||||
multiplayer.hint.details=查看詳情
|
||||
|
||||
nbt.entries=%s 個條目
|
||||
nbt.open.failed=打開檔案失敗
|
||||
nbt.save.failed=保存檔案失敗
|
||||
nbt.title=查看檔案 - %s
|
||||
|
||||
datapack=資料包
|
||||
datapack.add=加入資料包
|
||||
datapack.choose_datapack=選擇要匯入的資料包壓縮檔
|
||||
|
||||
@@ -779,6 +779,11 @@ multiplayer=多人联机
|
||||
multiplayer.hint=多人联机服务正在维护中。
|
||||
multiplayer.hint.details=查看详情
|
||||
|
||||
nbt.entries=%s 个条目
|
||||
nbt.open.failed=打开文件失败
|
||||
nbt.save.failed=保存文件失败
|
||||
nbt.title=查看文件 - %s
|
||||
|
||||
datapack=数据包
|
||||
datapack.add=添加数据包
|
||||
datapack.choose_datapack=选择要导入的数据包压缩包
|
||||
|
||||