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 index fca15907bd..d66c8c5753 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTEditorPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTEditorPage.java @@ -22,8 +22,14 @@ import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.geometry.Insets; import javafx.geometry.Pos; +import javafx.scene.control.Skin; +import javafx.scene.control.TreeItem; +import javafx.scene.control.TreeView; +import javafx.scene.control.skin.TreeViewSkin; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; +import org.glavo.nbt.NBTElement; +import org.glavo.nbt.tag.Tag; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.Controllers; @@ -33,6 +39,7 @@ import org.jackhuang.hmcl.ui.construct.PageCloseEvent; import org.jackhuang.hmcl.ui.construct.SpinnerPane; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.io.FileUtils; import java.io.IOException; import java.nio.file.Path; @@ -56,8 +63,9 @@ public final class NBTEditorPage extends SpinnerPane implements DecoratorPage { this.state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("nbt.title", file.toString()))); this.file = file; - this.type = NBTFileType.ofFile(file); + //noinspection DataFlowIssue + this.type = NBTFileType.ofFile(file); if (type == null) { throw new IOException("Unknown type of file " + file); } @@ -85,14 +93,33 @@ public final class NBTEditorPage extends SpinnerPane implements DecoratorPage { actions.getChildren().setAll(saveButton, cancelButton); - Task.supplyAsync(() -> type.readAsTree(file)) + Task.supplyAsync(() -> type.read(file)) .whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { setLoading(false); - NBTTreeView view = new NBTTreeView(result); + + NBTTreeItem root = new NBTTreeItem(result, FileUtils.getName(file)); + var view = new TreeView<>(root) { + @Override + protected Skin createDefaultSkin() { + return new TreeViewSkin<>(this) { + { + FXUtils.smoothScrolling(getVirtualFlow()); + } + }; + } + }; + view.setCellFactory(ignored -> new NBTTreeCell()); + view.addEventHandler(TreeItem.branchExpandedEvent(), event -> { + TreeItem item = event.getTreeItem(); + if (item.getValue() instanceof Tag && item.getChildren().size() == 1) + item.getChildren().get(0).setExpanded(true); + }); + root.setExpanded(true); + BorderPane.setMargin(view, new Insets(10)); onEscPressed(view, cancelButton::fire); - root.setCenter(view); + this.root.setCenter(view); } else { LOG.warning("Fail to open nbt file", exception); Controllers.dialog(i18n("nbt.open.failed") + "\n\n" + StringUtils.getStackTrace(exception), null, MessageDialogPane.MessageType.WARNING, cancelButton::fire); 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 index ceab275040..698fdd2241 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTFileType.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTFileType.java @@ -17,148 +17,34 @@ */ 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 kala.compress.utils.BoundedInputStream; +import org.glavo.nbt.NBTElement; +import org.glavo.nbt.chunk.ChunkRegion; +import org.glavo.nbt.io.NBTCodec; +import org.glavo.nbt.tag.Tag; import org.jackhuang.hmcl.util.io.FileUtils; +import org.jetbrains.annotations.Nullable; -import java.io.BufferedInputStream; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.RandomAccessFile; -import java.nio.file.Files; import java.nio.file.Path; -import java.util.Arrays; -import java.util.zip.GZIPInputStream; -import java.util.zip.Inflater; -import java.util.zip.InflaterInputStream; -/** - * @author Glavo - */ +/// @author Glavo public enum NBTFileType { COMPRESSED("dat", "dat_old") { @Override public Tag read(Path file) throws IOException { - try (BufferedInputStream fileInputStream = new BufferedInputStream(Files.newInputStream(file))) { - fileInputStream.mark(3); - byte[] header = new byte[3]; - if (fileInputStream.read(header) < 3) { - throw new IOException("File is too small"); - } - fileInputStream.reset(); - - InputStream input; - if (Arrays.equals(header, new byte[]{0x1f, (byte) 0x8b, 0x08})) { - input = new GZIPInputStream(fileInputStream); - } else { - input = fileInputStream; - } - - Tag tag = NBTIO.readTag(input); - if (!(tag instanceof CompoundTag)) - throw new IOException("Unexpected tag: " + tag); - return tag; - } + return NBTCodec.of().readTag(file); } }, ANVIL("mca") { @Override - public Tag read(Path file) throws IOException { - return REGION.read(file); - } - - @Override - public NBTTreeView.Item readAsTree(Path file) throws IOException { - return REGION.readAsTree(file); + public NBTElement read(Path file) throws IOException { + return NBTCodec.of().readRegion(file); } }, REGION("mcr") { @Override - public Tag read(Path file) throws IOException { - try (RandomAccessFile r = new RandomAccessFile(file.toFile(), "r")) { - ListTag tag = new ListTag(file.getFileName().toString(), CompoundTag.class); - if (r.length() == 0) { - return tag; - } - - byte[] header = new byte[4096]; - byte[] buffer = new byte[1 * 1024 * 1024]; // The maximum size of each chunk is 1MiB - Inflater inflater = new Inflater(); - - 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(Path 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; + public ChunkRegion read(Path file) throws IOException { + return NBTCodec.of().readRegion(file); } }; @@ -168,7 +54,7 @@ public enum NBTFileType { return NBTFileType.ofFile(file) != null; } - public static NBTFileType ofFile(Path file) { + public static @Nullable NBTFileType ofFile(Path file) { String ext = FileUtils.getExtension(file); for (NBTFileType type : types) { for (String extension : type.extensions) { @@ -186,11 +72,6 @@ public enum NBTFileType { this.extensions = extensions; } - public abstract Tag read(Path file) throws IOException; + public abstract NBTElement read(Path file) throws IOException; - public NBTTreeView.Item readAsTree(Path file) throws IOException { - NBTTreeView.Item root = NBTTreeView.buildTree(read(file)); - root.setName(file.getFileName().toString()); - return root; - } } 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 deleted file mode 100644 index f6084a037f..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTTagType.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2025 huangyuhui 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 . - */ -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; - -/** - * @author Glavo - */ -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/NBTTreeCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTTreeCell.java new file mode 100644 index 0000000000..95d877cbf7 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTTreeCell.java @@ -0,0 +1,129 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.ui.nbt; + +import javafx.scene.control.TreeCell; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import org.glavo.nbt.NBTElement; +import org.glavo.nbt.chunk.Chunk; +import org.glavo.nbt.chunk.ChunkRegion; +import org.glavo.nbt.tag.*; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jetbrains.annotations.Nullable; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public final class NBTTreeCell extends TreeCell<@Nullable NBTElement> { + + private static @Nullable Image getIcon(NBTElement element) { + if (element instanceof Tag tag) { + TagType type = tag.getType(); + if (type == TagType.BYTE) { + return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Byte.png"); + } else if (type == TagType.SHORT) { + return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Short.png"); + } else if (type == TagType.INT) { + return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Int.png"); + } else if (type == TagType.LONG) { + return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Long.png"); + } else if (type == TagType.FLOAT) { + return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Float.png"); + } else if (type == TagType.DOUBLE) { + return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Double.png"); + } else if (type == TagType.STRING) { + return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_String.png"); + } else if (type == TagType.BYTE_ARRAY) { + return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Byte_Array.png"); + } else if (type == TagType.INT_ARRAY) { + return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Int_Array.png"); + } else if (type == TagType.LONG_ARRAY) { + return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Long_Array.png"); + } else if (type == TagType.LIST) { + return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_List.png"); + } else if (type == TagType.COMPOUND) { + return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Compound.png"); + } else { + return null; + } + } else if (element instanceof ChunkRegion) + return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_List.png"); + else if (element instanceof Chunk) + return FXUtils.newBuiltinImage("/assets/img/nbt/TAG_Compound.png"); + else + return null; + } + + private final ImageView imageView; + + public NBTTreeCell() { + imageView = new ImageView(); + imageView.setFitHeight(16); + imageView.setFitWidth(16); + setGraphic(imageView); + } + + private NBTTreeItem getNBTTreeItem() { + return (NBTTreeItem) getTreeItem(); + } + + @Override + public void updateItem(@Nullable NBTElement item, boolean empty) { + super.updateItem(item, empty); + + if (empty || item == null) { + imageView.setImage(null); + setText(null); + return; + } + + imageView.setImage(getIcon(item)); + + String name = getNBTTreeItem().getOverrideName(); + if (name == null) { + NBTElement value = getNBTTreeItem().getValue(); + + if (value instanceof Tag tag) { + if (tag.getParent() instanceof ListTag) { + name = Integer.toString(tag.getIndex()); + } else { + name = tag.getName(); + } + } else if (value instanceof Chunk chunk) { + name = "Chunk (" + chunk.getLocalX() + ", " + chunk.getLocalZ() + ")"; + } else { + name = ""; + } + } + + String text; + if (item instanceof ParentTag parentTag) { + text = i18n("nbt.entries", parentTag.size()); + } else if (item instanceof ValueTag valueTag) { + text = valueTag.getAsString(); + } else { + text = null; + } + + if (text == null) { + setText(name); + } else { + setText(name + ": " + text); + } + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTTreeItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTTreeItem.java new file mode 100644 index 0000000000..7b1f8a029e --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTTreeItem.java @@ -0,0 +1,68 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.ui.nbt; + +import javafx.collections.ObservableList; +import javafx.scene.control.TreeItem; +import org.glavo.nbt.NBTElement; +import org.glavo.nbt.NBTParent; +import org.glavo.nbt.chunk.Chunk; +import org.glavo.nbt.tag.*; +import org.jetbrains.annotations.Nullable; + +/// @author Glavo +public final class NBTTreeItem extends TreeItem { + private final @Nullable String overrideName; + + public NBTTreeItem(NBTElement value) { + this(value, null); + } + + public NBTTreeItem(NBTElement value, @Nullable String overrideName) { + super(value); + this.overrideName = overrideName; + } + + @Override + public boolean isLeaf() { + return !(getValue() instanceof NBTParent parent) || parent.isEmpty(); + } + + private boolean needInitChildren = true; + + @Override + public ObservableList> getChildren() { + ObservableList> children = super.getChildren(); + if (needInitChildren) { + needInitChildren = false; + + if (getValue() instanceof Chunk chunk) { + if (chunk.getRootTag() != null) { + children.setAll(chunk.getRootTag().stream().map(NBTTreeItem::new).toList()); + } + } else if (getValue() instanceof NBTParent parent) { + children.setAll(parent.stream().map(NBTTreeItem::new).toList()); + } + } + return children; + } + + public @Nullable String getOverrideName() { + return overrideName; + } +} 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 deleted file mode 100644 index 63f3557ea8..0000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTTreeView.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2025 huangyuhui 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 . - */ -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.TreeCell; -import javafx.scene.control.TreeItem; -import javafx.scene.control.TreeView; -import javafx.scene.control.skin.TreeViewSkin; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.util.Callback; -import org.jackhuang.hmcl.ui.FXUtils; - -import java.lang.reflect.Array; -import java.util.EnumMap; - -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; - -/** - * @author Glavo - */ -public final class NBTTreeView extends TreeView { - - public NBTTreeView(NBTTreeView.Item tree) { - this.setRoot(tree); - if (tree != null) tree.setExpanded(true); - this.setCellFactory(cellFactory()); - } - - @Override - protected javafx.scene.control.Skin createDefaultSkin() { - return new TreeViewSkin(this) { - { - FXUtils.smoothScrolling(getVirtualFlow()); - } - }; - } - - private static Callback, TreeCell> cellFactory() { - 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); - - 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()))); - imageView.setFitHeight(16); - imageView.setFitWidth(16); - - 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); - } - } - FXUtils.onChangeAndOperate(item.expandedProperty(), expanded -> { - if (expanded && item.getChildren().size() == 1) item.getChildren().get(0).setExpanded(true); - }); - - 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/java/org/jackhuang/hmcl/ui/nbt/package-info.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/package-info.java new file mode 100644 index 0000000000..5821780349 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/package-info.java @@ -0,0 +1,22 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ + +@NotNullByDefault +package org.jackhuang.hmcl.ui.nbt; + +import org.jetbrains.annotations.NotNullByDefault; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java index 89ffbae4db..16c633fbc1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java @@ -17,7 +17,6 @@ */ package org.jackhuang.hmcl.ui.versions; -import com.github.steveice10.opennbt.tag.builtin.*; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXTextField; import javafx.beans.property.SimpleBooleanProperty; @@ -34,6 +33,7 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.stage.FileChooser; +import org.glavo.nbt.tag.*; import org.jackhuang.hmcl.game.World; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; @@ -76,7 +76,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage. } private void updateControls() { - CompoundTag dataTag = levelData.get("Data"); + CompoundTag dataTag = (CompoundTag) levelData.get("Data"); CompoundTag playerTag = playerData; ScrollPane scrollPane = new ScrollPane(); @@ -100,7 +100,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage. setRightTextField(worldNamePane, worldNameField, 200); if (dataTag.get("LevelName") instanceof StringTag worldNameTag) { - var worldName = new SimpleStringProperty(worldNameTag.getValue()); + var worldName = new SimpleStringProperty(worldNameTag.get()); FXUtils.bindString(worldNameField, worldName); worldNameField.getProperties().put(WorldInfoPage.class.getName() + ".worldNameProperty", worldName); worldName.addListener((observable, oldValue, newValue) -> { @@ -324,7 +324,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage. { locationPane.setTitle(i18n("world.info.player.location")); Dimension dimension = Dimension.of(playerTag.get("Dimension")); - if (dimension != null && playerTag.get("Pos") instanceof ListTag posTag) { + if (dimension != null && playerTag.get("Pos") instanceof ListTag posTag) { locationPane.setText(dimension.formatPosition(posTag)); } } @@ -347,7 +347,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage. 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 + && intArrayTag.size() >= 3) { // Valid after 25w07a spawnPane.setText(Dimension.of(dimensionTag).formatPosition(intArrayTag)); } else if (playerTag.get("SpawnX") instanceof IntTag intX && playerTag.get("SpawnY") instanceof IntTag intY @@ -541,7 +541,7 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage. static Dimension of(Tag tag) { if (tag instanceof IntTag intTag) { - return switch (intTag.getValue()) { + return switch (intTag.get()) { case 0 -> OVERWORLD; case -1 -> THE_NETHER; case 1 -> THE_END; @@ -561,30 +561,23 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage. } String formatPosition(Tag tag) { - if (tag instanceof ListTag listTag && listTag.size() == 3) { + if (tag instanceof ListTag listTag && listTag.size() == 3) { - 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) { - 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()); + if (listTag.getTag(0) instanceof DoubleTag x + && listTag.getTag(1) instanceof DoubleTag y + && listTag.getTag(2) instanceof DoubleTag z) { + return formatPosition(x.get(), y.get(), z.get()); } return null; } if (tag instanceof IntArrayTag intArrayTag) { + int x = intArrayTag.get(0); + int y = intArrayTag.get(1); + int z = intArrayTag.get(2); - 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 formatPosition(x, y, z); } return null; @@ -592,14 +585,14 @@ public final class WorldInfoPage extends SpinnerPane implements WorldManagePage. 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); + ? "(%d, %d, %d)".formatted(x, y, z) + : "%s (%d, %d, %d)".formatted(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); + ? "(%.2f, %.2f, %.2f)".formatted(x, y, z) + : "%s (%.2f, %.2f, %.2f)".formatted(name, x, y, z); } } diff --git a/HMCL/src/main/resources/assets/about/deps.json b/HMCL/src/main/resources/assets/about/deps.json index d184888c13..4b157f75ac 100644 --- a/HMCL/src/main/resources/assets/about/deps.json +++ b/HMCL/src/main/resources/assets/about/deps.json @@ -45,9 +45,9 @@ "externalLink" : "https://github.com/jenkinsci/constant-pool-scanner" }, { - "title" : "OpenNBT", - "subtitle" : "Copyright © 2013-2021 Steveice10.\nLicensed under the MIT License.", - "externalLink" : "https://github.com/GeyserMC/OpenNBT" + "title" : "HelloNBT", + "subtitle" : "Copyright © 2026 Glavo.\nLicensed under the Apache 2.0 License.", + "externalLink" : "https://github.com/Glavo/HelloNBT" }, { "title" : "minecraft-jfx-skin", diff --git a/HMCLCore/build.gradle.kts b/HMCLCore/build.gradle.kts index 86ca2bde92..284e3d7d7e 100644 --- a/HMCLCore/build.gradle.kts +++ b/HMCLCore/build.gradle.kts @@ -18,14 +18,15 @@ dependencies { api(libs.gson) api(libs.toml) api(libs.xz) + api(libs.lz4) api(libs.fx.gson) api(libs.constant.pool.scanner) - api(libs.opennbt) api(libs.nanohttpd) api(libs.jsoup) api(libs.chardet) api(libs.jna) api(libs.pci.ids) + api(libs.hello.nbt) compileOnlyApi(libs.jetbrains.annotations) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java index b3fcb96b33..e3f4d57da9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java @@ -17,9 +17,9 @@ */ package org.jackhuang.hmcl.game; -import com.github.steveice10.opennbt.NBTIO; -import com.github.steveice10.opennbt.tag.builtin.*; import javafx.scene.image.Image; +import org.glavo.nbt.io.NBTCodec; +import org.glavo.nbt.tag.*; import org.jackhuang.hmcl.util.io.*; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.jetbrains.annotations.Nullable; @@ -34,9 +34,7 @@ import java.nio.channels.OverlappingFileLockException; import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.util.List; -import java.util.UUID; import java.util.stream.Stream; -import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -48,6 +46,7 @@ public final class World { private Image icon; private CompoundTag levelData; + private CompoundTag dataTag; private Path levelDataPath; private CompoundTag worldGenSettingsDataBackingTag; // Use for writing back to the file @@ -60,10 +59,64 @@ public final class World { public World(Path file) throws IOException { this.file = file; - if (Files.isDirectory(file)) - loadFromDirectory(); - else if (Files.isRegularFile(file)) - loadFromZip(); + if (Files.isDirectory(file)) { + fileName = FileUtils.getName(this.file); + Path levelDatPath = this.file.resolve("level.dat"); + if (!Files.exists(levelDatPath)) { // version 20w14infinite + levelDatPath = this.file.resolve("special_level.dat"); + } + if (!Files.exists(levelDatPath)) { + throw new IOException("Not a valid world directory since level.dat or special_level.dat cannot be found."); + } + this.levelDataPath = levelDatPath; + loadAndCheckWorldData(); + + Path iconFile = this.file.resolve("icon.png"); + if (Files.isRegularFile(iconFile)) { + try (InputStream inputStream = Files.newInputStream(iconFile)) { + icon = new Image(inputStream, 64, 64, true, false); + if (icon.isError()) + throw icon.getException(); + } catch (Exception e) { + LOG.warning("Failed to load world icon", e); + } + } + } else if (Files.isRegularFile(file)) + try (FileSystem fs = CompressingUtils.readonly(this.file).setAutoDetectEncoding(true).build()) { + Path root; + if (Files.isRegularFile(fs.getPath("/level.dat"))) { + root = fs.getPath("/"); + fileName = FileUtils.getName(this.file); + } else { + List files = Files.list(fs.getPath("/")).toList(); + if (files.size() != 1 || !Files.isDirectory(files.get(0))) { + throw new IOException("Not a valid world zip file"); + } + + root = files.get(0); + fileName = FileUtils.getName(root); + } + + Path levelDat = root.resolve("level.dat"); + if (!Files.exists(levelDat)) { //version 20w14infinite + levelDat = root.resolve("special_level.dat"); + } + if (!Files.exists(levelDat)) { + throw new IOException("Not a valid world zip file since level.dat or special_level.dat cannot be found."); + } + loadAndCheckLevelData(levelDat); + + Path iconFile = root.resolve("icon.png"); + if (Files.isRegularFile(iconFile)) { + try (InputStream inputStream = Files.newInputStream(iconFile)) { + icon = new Image(inputStream, 64, 64, true, false); + if (icon.isError()) + throw icon.getException(); + } catch (Exception e) { + LOG.warning("Failed to load world icon", e); + } + } + } else throw new IOException("Path " + file + " cannot be recognized as a Minecraft world"); } @@ -77,9 +130,11 @@ public final class World { } public String getWorldName() { - CompoundTag data = levelData.get("Data"); - StringTag levelNameTag = data.get("LevelName"); - return levelNameTag.getValue(); + if (levelData.get("Data") instanceof CompoundTag data + && data.get("LevelName") instanceof StringTag levelNameTag) + return levelNameTag.get(); + else + return ""; } public void setWorldName(String worldName) throws IOException { @@ -106,9 +161,11 @@ public final class World { } public long getLastPlayed() { - CompoundTag data = levelData.get("Data"); - LongTag lastPlayedTag = data.get("LastPlayed"); - return lastPlayedTag.getValue(); + if (dataTag.get("LastPlayed") instanceof LongTag lastPlayedTag) { + return lastPlayedTag.get(); + } else { + return 0L; + } } public @Nullable GameVersionNumber getGameVersion() { @@ -122,21 +179,20 @@ public final class World { public @Nullable Long getSeed() { // Valid after 1.16(20w20a) - if (normalizedWorldGenSettingsData != null && normalizedWorldGenSettingsData.get("seed") instanceof LongTag seedTag) { + if (normalizedWorldGenSettingsData != null + && normalizedWorldGenSettingsData.get("seed") instanceof LongTag seedTag) { return seedTag.getValue(); } // Valid before 1.16(20w20a) - if (levelData.get("Data") instanceof CompoundTag data && data.get("RandomSeed") instanceof LongTag seedTag) { + if (dataTag.get("RandomSeed") instanceof LongTag seedTag) { return seedTag.getValue(); } return null; } public boolean isLargeBiomes() { - CompoundTag data = levelData.get("Data"); - // Valid before 1.16(20w20a) - if (data.get("generatorName") instanceof StringTag generatorNameTag) { + if (dataTag.get("generatorName") instanceof StringTag generatorNameTag) { return "largeBiomes".equals(generatorNameTag.getValue()); } // Unified handling of logic after version 1.16 @@ -147,11 +203,11 @@ public final class World { // Valid between 1.16(20w20a) and 1.18(21w37a) if (generatorTag.get("biome_source") instanceof CompoundTag biomeSourceTag && biomeSourceTag.get("large_biomes") instanceof ByteTag largeBiomesTag) { - return largeBiomesTag.getValue() == (byte) 1; + return largeBiomesTag.get() == (byte) 1; } // Valid after 1.18(21w37a) else if (generatorTag.get("settings") instanceof StringTag settingsTag) { - return "minecraft:large_biomes".equals(settingsTag.getValue()); + return "minecraft:large_biomes".equals(settingsTag.get()); } } } @@ -178,79 +234,14 @@ public final class World { return gameVersionNumber != null && gameVersionNumber.isAtLeast("1.20", "23w14a"); } - private void loadFromDirectory() throws IOException { - fileName = FileUtils.getName(file); - Path levelDat = file.resolve("level.dat"); - 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."); - } - this.levelDataPath = levelDat; - loadAndCheckWorldData(); - - Path iconFile = file.resolve("icon.png"); - if (Files.isRegularFile(iconFile)) { - try (InputStream inputStream = Files.newInputStream(iconFile)) { - icon = new Image(inputStream, 64, 64, true, false); - if (icon.isError()) - throw icon.getException(); - } catch (Exception e) { - LOG.warning("Failed to load world icon", e); - } - } - } - - private void loadFromZipImpl(Path root) throws IOException { - Path levelDat = root.resolve("level.dat"); - if (!Files.exists(levelDat)) { //version 20w14infinite - levelDat = root.resolve("special_level.dat"); - } - if (!Files.exists(levelDat)) { - throw new IOException("Not a valid world zip file since level.dat or special_level.dat cannot be found."); - } - loadAndCheckLevelData(levelDat); - - Path iconFile = root.resolve("icon.png"); - if (Files.isRegularFile(iconFile)) { - try (InputStream inputStream = Files.newInputStream(iconFile)) { - icon = new Image(inputStream, 64, 64, true, false); - if (icon.isError()) - throw icon.getException(); - } catch (Exception e) { - LOG.warning("Failed to load world icon", e); - } - } - } - - private void loadFromZip() throws IOException { - try (FileSystem fs = CompressingUtils.readonly(file).setAutoDetectEncoding(true).build()) { - Path levelDatPath = fs.getPath("/level.dat"); - if (Files.isRegularFile(levelDatPath)) { - fileName = FileUtils.getName(file); - loadFromZipImpl(fs.getPath("/")); - return; - } - - try (Stream stream = Files.list(fs.getPath("/"))) { - Path root = stream.filter(Files::isDirectory).findAny() - .orElseThrow(() -> new IOException("Not a valid world zip file")); - fileName = FileUtils.getName(root); - loadFromZipImpl(root); - } - } - } - private void loadAndCheckWorldData() throws IOException { loadAndCheckLevelData(levelDataPath); loadOtherData(); } private void loadAndCheckLevelData(Path levelDat) throws IOException { - this.levelData = readTag(levelDat); - CompoundTag data = levelData.get("Data"); - if (data == null) + this.levelData = NBTCodec.of().readTag(levelDat, TagType.COMPOUND); + if (!(levelData.get("Data") instanceof CompoundTag data)) throw new IOException("level.dat missing Data"); if (!(data.get("LevelName") instanceof StringTag)) @@ -258,6 +249,7 @@ public final class World { if (!(data.get("LastPlayed") instanceof LongTag)) throw new IOException("level.dat missing LastPlayed"); + this.dataTag = data; } private void loadOtherData() throws IOException { @@ -267,7 +259,7 @@ public final class World { if (data.get("WorldGenSettings") instanceof CompoundTag worldGenSettingsTag) { setWorldGenSettingsData(null, worldGenSettingsTag, worldGenSettingsTag); } else if (Files.isRegularFile(worldGenSettingsDatPath)) { - CompoundTag raw = readTag(worldGenSettingsDatPath); + CompoundTag raw = NBTCodec.of().readTag(worldGenSettingsDatPath, TagType.COMPOUND); if (raw.get("data") instanceof CompoundTag compoundTag) { setWorldGenSettingsData(worldGenSettingsDatPath, raw, compoundTag); } else { @@ -279,14 +271,11 @@ public final class World { if (data.get("Player") instanceof CompoundTag playerTag) { setPlayerData(null, playerTag); - } else if (data.get("singleplayer_uuid") instanceof IntArrayTag uuidTag && uuidTag.getValue().length == 4) { - int[] uuidValue = uuidTag.getValue(); - long mostSigBits = ((long) uuidValue[0] << 32) | (uuidValue[1] & 0xFFFFFFFFL); - long leastSigBits = ((long) uuidValue[2] << 32) | (uuidValue[3] & 0xFFFFFFFFL); - String playerUUID = new UUID(mostSigBits, leastSigBits).toString(); + } else if (data.get("singleplayer_uuid") instanceof IntArrayTag uuidTag && uuidTag.isUUID()) { + String playerUUID = uuidTag.getUUID().toString(); Path playerDatPath = file.resolve("players/data/" + playerUUID + ".dat"); if (Files.exists(playerDatPath)) { - setPlayerData(playerDatPath, readTag(playerDatPath)); + setPlayerData(playerDatPath, NBTCodec.of().readTag(playerDatPath, TagType.COMPOUND)); } else { setPlayerData(null, null); } @@ -317,8 +306,7 @@ public final class World { throw new IOException("Not a valid world directory"); // Change the name recorded in level.dat - CompoundTag data = levelData.get("Data"); - data.put(new StringTag("LevelName", newName)); + dataTag.setString("LevelName", newName); writeLevelData(); // then change the folder's name @@ -433,19 +421,11 @@ public final class World { writeTag(levelData, levelDataPath); } - private CompoundTag readTag(Path path) throws IOException { - try (InputStream is = new GZIPInputStream(Files.newInputStream(path))) { - Tag tag = NBTIO.readTag(is); - if (tag instanceof CompoundTag compoundTag) return compoundTag; - throw new IOException("NBT file malformed: " + path); - } - } - private void writeTag(CompoundTag nbt, Path path) throws IOException { if (!Files.isDirectory(file)) throw new IOException("Not a valid world directory"); FileUtils.saveSafely(path, os -> { try (OutputStream gos = new GZIPOutputStream(os)) { - NBTIO.writeTag(gos, nbt); + NBTCodec.of().writeTag(gos, nbt); } }); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java index 3ad27a3692..e47feafde9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java @@ -17,9 +17,9 @@ */ package org.jackhuang.hmcl.schematic; -import com.github.steveice10.opennbt.NBTIO; -import com.github.steveice10.opennbt.tag.builtin.*; import javafx.geometry.Point3D; +import org.glavo.nbt.io.NBTCodec; +import org.glavo.nbt.tag.*; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -30,32 +30,19 @@ import java.nio.file.Path; import java.time.Instant; import java.util.zip.GZIPInputStream; -/** - * @author Glavo - * @see The Litematic file format - */ +/// @author Glavo +/// @see The Litematic file format public final class LitematicFile { - private static int tryGetInt(Tag tag) { - return tag instanceof IntTag ? ((IntTag) tag).getValue() : 0; - } - - private static @Nullable Instant tryGetLongTimestamp(Tag tag) { - if (tag instanceof LongTag) { - return Instant.ofEpochMilli(((LongTag) tag).getValue()); - } - return null; - } - private static @Nullable String tryGetString(Tag tag) { - return tag instanceof StringTag ? ((StringTag) tag).getValue() : null; + return tag instanceof StringTag stringTag ? stringTag.get() : null; } public static LitematicFile load(Path file) throws IOException { CompoundTag root; try (InputStream in = new GZIPInputStream(Files.newInputStream(file))) { - root = (CompoundTag) NBTIO.readTag(in); + root = NBTCodec.of().readTag(in, TagType.COMPOUND); } Tag versionTag = root.get("Version"); @@ -71,14 +58,13 @@ public final class LitematicFile { throw new IOException("Metadata tag is not a compound tag"); int regions = 0; - Tag regionsTag = root.get("Regions"); - if (regionsTag instanceof CompoundTag) - regions = ((CompoundTag) regionsTag).size(); + if (root.get("Regions") instanceof CompoundTag regionsTag) + regions = regionsTag.size(); return new LitematicFile(file, (CompoundTag) metadataTag, ((IntTag) versionTag).getValue(), - tryGetInt(root.get("SubVersion")), - tryGetInt(root.get("MinecraftDataVersion")), + root.getIntOrZero("SubVersion"), + root.getIntOrZero("MinecraftDataVersion"), regions ); } @@ -108,26 +94,23 @@ public final class LitematicFile { this.regionCount = regionCount; Tag previewImageData = metadata.get("PreviewImageData"); - this.previewImageData = previewImageData instanceof IntArrayTag - ? ((IntArrayTag) previewImageData).getValue() + this.previewImageData = previewImageData instanceof IntArrayTag intArrayTag + ? intArrayTag.getArray() : null; this.name = tryGetString(metadata.get("Name")); this.author = tryGetString(metadata.get("Author")); this.description = tryGetString(metadata.get("Description")); - this.timeCreated = tryGetLongTimestamp(metadata.get("TimeCreated")); - this.timeModified = tryGetLongTimestamp(metadata.get("TimeModified")); - this.totalBlocks = tryGetInt(metadata.get("TotalBlocks")); - this.totalVolume = tryGetInt(metadata.get("TotalVolume")); - + this.timeCreated = metadata.get("TimeCreated") instanceof LongTag time ? Instant.ofEpochMilli(time.getValue()) : null; + this.timeModified = metadata.get("TimeModified") instanceof LongTag time ? Instant.ofEpochMilli(time.getValue()) : null; + this.totalBlocks = metadata.getIntOrZero("TotalBlocks"); + this.totalVolume = metadata.getIntOrZero("TotalVolume"); Point3D enclosingSize = null; - Tag enclosingSizeTag = metadata.get("EnclosingSize"); - if (enclosingSizeTag instanceof CompoundTag) { - CompoundTag list = (CompoundTag) enclosingSizeTag; - int x = tryGetInt(list.get("x")); - int y = tryGetInt(list.get("y")); - int z = tryGetInt(list.get("z")); + if (metadata.get("EnclosingSize") instanceof CompoundTag list) { + int x = list.getIntOrZero("x"); + int y = list.getIntOrZero("y"); + int z = list.getIntOrZero("z"); if (x >= 0 && y >= 0 && z >= 0) enclosingSize = new Point3D(x, y, z); diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 16e5c38c3c..357f3b608f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,9 +6,10 @@ simple-png-javafx = "0.3.0" gson = "2.13.2" toml4j = "0.7.2" xz = "1.11" +lz4 = "1.10.4.1" fx-gson = "5.0.0" constant-pool-scanner = "1.2" -opennbt = "1.5" +hello-nbt = "0.2.0" nanohttpd = "2.3.1" jsoup = "1.21.2" chardet = "2.5.0" @@ -37,9 +38,10 @@ simple-png-javafx = { module = "org.glavo:simple-png-javafx", version.ref = "sim gson = { module = "com.google.code.gson:gson", version.ref = "gson" } toml = { module = "com.moandjiezana.toml:toml4j", version.ref = "toml4j" } xz = { module = "org.tukaani:xz", version.ref = "xz" } +lz4 = { module = "org.glavo:lz4-java", version.ref = "lz4" } fx-gson = { module = "org.hildan.fxgson:fx-gson", version.ref = "fx-gson" } constant-pool-scanner = { module = "org.jenkins-ci:constant-pool-scanner", version.ref = "constant-pool-scanner" } -opennbt = { module = "com.github.steveice10:opennbt", version.ref = "opennbt" } +hello-nbt = { module = "org.glavo:HelloNBT", version.ref = "hello-nbt" } nanohttpd = { module = "org.nanohttpd:nanohttpd", version.ref = "nanohttpd" } jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } chardet = { module = "org.glavo:chardet", version.ref = "chardet" }