diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java index e4b4b5cfa..d6dd1c5fb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java @@ -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()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTEditorPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTEditorPage.java new file mode 100644 index 000000000..3840c693d --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTEditorPage.java @@ -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; + 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 stateProperty() { + return state; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTFileType.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTFileType.java new file mode 100644 index 000000000..4b35a6693 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTFileType.java @@ -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; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTHelper.java new file mode 100644 index 000000000..84a533a07 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTHelper.java @@ -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; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTTagType.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTTagType.java new file mode 100644 index 000000000..65c4de7b9 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTTagType.java @@ -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 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; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTTreeView.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTTreeView.java new file mode 100644 index 000000000..ff61fb57c --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTTreeView.java @@ -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 { + + public NBTTreeView(NBTTreeView.Item tree) { + this.setRoot(tree); + this.setCellFactory(cellFactory()); + } + + private static Callback, TreeCell> cellFactory() { + Holder lastCell = new Holder<>(); + EnumMap icons = new EnumMap<>(NBTTagType.class); + + return view -> new TreeCell() { + 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 { + + 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; + } + } +} diff --git a/HMCL/src/main/resources/assets/img/nbt/TAG_Byte.png b/HMCL/src/main/resources/assets/img/nbt/TAG_Byte.png new file mode 100644 index 000000000..02ef065ad Binary files /dev/null and b/HMCL/src/main/resources/assets/img/nbt/TAG_Byte.png differ diff --git a/HMCL/src/main/resources/assets/img/nbt/TAG_Byte@2x.png b/HMCL/src/main/resources/assets/img/nbt/TAG_Byte@2x.png new file mode 100644 index 000000000..d74c1bd56 Binary files /dev/null and b/HMCL/src/main/resources/assets/img/nbt/TAG_Byte@2x.png differ diff --git a/HMCL/src/main/resources/assets/img/nbt/TAG_Byte_Array.png b/HMCL/src/main/resources/assets/img/nbt/TAG_Byte_Array.png new file mode 100644 index 000000000..5639fde91 Binary files /dev/null and b/HMCL/src/main/resources/assets/img/nbt/TAG_Byte_Array.png differ diff --git a/HMCL/src/main/resources/assets/img/nbt/TAG_Byte_Array@2x.png b/HMCL/src/main/resources/assets/img/nbt/TAG_Byte_Array@2x.png new file mode 100644 index 000000000..c9b3aa4df Binary files /dev/null and b/HMCL/src/main/resources/assets/img/nbt/TAG_Byte_Array@2x.png differ diff --git a/HMCL/src/main/resources/assets/img/nbt/TAG_Compound.png b/HMCL/src/main/resources/assets/img/nbt/TAG_Compound.png new file mode 100644 index 000000000..dfd9d695b Binary files /dev/null and b/HMCL/src/main/resources/assets/img/nbt/TAG_Compound.png differ diff --git a/HMCL/src/main/resources/assets/img/nbt/TAG_Compound@2x.png b/HMCL/src/main/resources/assets/img/nbt/TAG_Compound@2x.png new file mode 100644 index 000000000..efa4da374 Binary files /dev/null and b/HMCL/src/main/resources/assets/img/nbt/TAG_Compound@2x.png differ diff --git a/HMCL/src/main/resources/assets/img/nbt/TAG_Double.png b/HMCL/src/main/resources/assets/img/nbt/TAG_Double.png new file mode 100644 index 000000000..e0bb5cc4e Binary files /dev/null and b/HMCL/src/main/resources/assets/img/nbt/TAG_Double.png differ diff --git a/HMCL/src/main/resources/assets/img/nbt/TAG_Double@2x.png b/HMCL/src/main/resources/assets/img/nbt/TAG_Double@2x.png new file mode 100644 index 000000000..6c344cca7 Binary files /dev/null and b/HMCL/src/main/resources/assets/img/nbt/TAG_Double@2x.png differ diff --git a/HMCL/src/main/resources/assets/img/nbt/TAG_Float.png b/HMCL/src/main/resources/assets/img/nbt/TAG_Float.png new file mode 100644 index 000000000..0f972a8c2 Binary files /dev/null and b/HMCL/src/main/resources/assets/img/nbt/TAG_Float.png differ diff --git a/HMCL/src/main/resources/assets/img/nbt/TAG_Float@2x.png b/HMCL/src/main/resources/assets/img/nbt/TAG_Float@2x.png new file mode 100644 index 000000000..6ed909054 Binary files /dev/null and b/HMCL/src/main/resources/assets/img/nbt/TAG_Float@2x.png differ diff --git a/HMCL/src/main/resources/assets/img/nbt/TAG_Int.png b/HMCL/src/main/resources/assets/img/nbt/TAG_Int.png new file mode 100644 index 000000000..5f8dba8fe Binary files /dev/null and b/HMCL/src/main/resources/assets/img/nbt/TAG_Int.png differ diff --git a/HMCL/src/main/resources/assets/img/nbt/TAG_Int@2x.png b/HMCL/src/main/resources/assets/img/nbt/TAG_Int@2x.png new file mode 100644 index 000000000..f6675ee4a Binary files /dev/null and b/HMCL/src/main/resources/assets/img/nbt/TAG_Int@2x.png differ diff --git a/HMCL/src/main/resources/assets/img/nbt/TAG_Int_Array.png b/HMCL/src/main/resources/assets/img/nbt/TAG_Int_Array.png new file mode 100644 index 000000000..db42591dd Binary files /dev/null and b/HMCL/src/main/resources/assets/img/nbt/TAG_Int_Array.png differ diff --git a/HMCL/src/main/resources/assets/img/nbt/TAG_Int_Array@2x.png b/HMCL/src/main/resources/assets/img/nbt/TAG_Int_Array@2x.png new file mode 100644 index 000000000..af42fb705 Binary files /dev/null and b/HMCL/src/main/resources/assets/img/nbt/TAG_Int_Array@2x.png differ diff --git a/HMCL/src/main/resources/assets/img/nbt/TAG_List.png b/HMCL/src/main/resources/assets/img/nbt/TAG_List.png new file mode 100644 index 000000000..37a8d3672 Binary files /dev/null and b/HMCL/src/main/resources/assets/img/nbt/TAG_List.png differ diff --git a/HMCL/src/main/resources/assets/img/nbt/TAG_List@2x.png b/HMCL/src/main/resources/assets/img/nbt/TAG_List@2x.png new file mode 100644 index 000000000..c3a9da46e Binary files /dev/null and b/HMCL/src/main/resources/assets/img/nbt/TAG_List@2x.png differ diff --git a/HMCL/src/main/resources/assets/img/nbt/TAG_Long.png b/HMCL/src/main/resources/assets/img/nbt/TAG_Long.png new file mode 100644 index 000000000..23800e48f Binary files /dev/null and b/HMCL/src/main/resources/assets/img/nbt/TAG_Long.png differ diff --git a/HMCL/src/main/resources/assets/img/nbt/TAG_Long@2x.png b/HMCL/src/main/resources/assets/img/nbt/TAG_Long@2x.png new file mode 100644 index 000000000..3e77c4bff Binary files /dev/null and b/HMCL/src/main/resources/assets/img/nbt/TAG_Long@2x.png differ diff --git a/HMCL/src/main/resources/assets/img/nbt/TAG_Long_Array.png b/HMCL/src/main/resources/assets/img/nbt/TAG_Long_Array.png new file mode 100644 index 000000000..8b962bc5c Binary files /dev/null and b/HMCL/src/main/resources/assets/img/nbt/TAG_Long_Array.png differ diff --git a/HMCL/src/main/resources/assets/img/nbt/TAG_Long_Array@2x.png b/HMCL/src/main/resources/assets/img/nbt/TAG_Long_Array@2x.png new file mode 100644 index 000000000..a66fab0e7 Binary files /dev/null and b/HMCL/src/main/resources/assets/img/nbt/TAG_Long_Array@2x.png differ diff --git a/HMCL/src/main/resources/assets/img/nbt/TAG_Short.png b/HMCL/src/main/resources/assets/img/nbt/TAG_Short.png new file mode 100644 index 000000000..1aa3396d4 Binary files /dev/null and b/HMCL/src/main/resources/assets/img/nbt/TAG_Short.png differ diff --git a/HMCL/src/main/resources/assets/img/nbt/TAG_Short@2x.png b/HMCL/src/main/resources/assets/img/nbt/TAG_Short@2x.png new file mode 100644 index 000000000..42c16799e Binary files /dev/null and b/HMCL/src/main/resources/assets/img/nbt/TAG_Short@2x.png differ diff --git a/HMCL/src/main/resources/assets/img/nbt/TAG_String.png b/HMCL/src/main/resources/assets/img/nbt/TAG_String.png new file mode 100644 index 000000000..732baeeac Binary files /dev/null and b/HMCL/src/main/resources/assets/img/nbt/TAG_String.png differ diff --git a/HMCL/src/main/resources/assets/img/nbt/TAG_String@2x.png b/HMCL/src/main/resources/assets/img/nbt/TAG_String@2x.png new file mode 100644 index 000000000..5f534e240 Binary files /dev/null and b/HMCL/src/main/resources/assets/img/nbt/TAG_String@2x.png differ diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 1644d6606..89b3eabcf 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -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 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 8c184bcc5..18747cb7f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -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=選擇要匯入的資料包壓縮檔 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 7d02fddce..7008c8293 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -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=选择要导入的数据包压缩包