diff --git a/build.gradle b/build.gradle index 1e6f45d..0ba09f8 100644 --- a/build.gradle +++ b/build.gradle @@ -39,7 +39,7 @@ buildscript { } } dependencies { - classpath 'net.saliman:gradle-cobertura-plugin:2.2.8' // Coveralls dependency + classpath 'net.saliman:gradle-cobertura-plugin:2.6.0' // Coveralls dependency classpath 'nl.javadude.gradle.plugins:license-gradle-plugin:0.10.0' classpath 'org.kt3k.gradle.plugin:coveralls-gradle-plugin:2.4.0' } @@ -83,8 +83,8 @@ license { // Source compiler configuration configure([compileJava, compileTestJava]) { - sourceCompatibility = '1.7' - targetCompatibility = '1.7' + sourceCompatibility = '1.8' + targetCompatibility = '1.8' options.encoding = 'UTF-8' options.compilerArgs << '-Xlint:all' options.compilerArgs << '-Xlint:-path' diff --git a/pom.xml b/pom.xml index fff3697..a799999 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ Flow NBT com.flowpowered flow-nbt - 1.0.1-SNAPSHOT + 1.0.2-SNAPSHOT jar 2011 https://flowpowered.com/nbt diff --git a/src/main/java/com/flowpowered/nbt/ByteArrayTag.java b/src/main/java/com/flowpowered/nbt/ByteArrayTag.java index 1c2d1bf..bad1352 100644 --- a/src/main/java/com/flowpowered/nbt/ByteArrayTag.java +++ b/src/main/java/com/flowpowered/nbt/ByteArrayTag.java @@ -32,7 +32,7 @@ public final class ByteArrayTag extends Tag { /** * The value. */ - private final byte[] value; + private byte[] value; /** * Creates the tag. @@ -50,6 +50,11 @@ public byte[] getValue() { return value; } + @Override + public void setValue(byte[] value) { + this.value = value; + } + @Override public String toString() { StringBuilder hex = new StringBuilder(); diff --git a/src/main/java/com/flowpowered/nbt/ByteTag.java b/src/main/java/com/flowpowered/nbt/ByteTag.java index d0232e3..284922d 100644 --- a/src/main/java/com/flowpowered/nbt/ByteTag.java +++ b/src/main/java/com/flowpowered/nbt/ByteTag.java @@ -30,7 +30,7 @@ public final class ByteTag extends Tag { /** * The value. */ - private final byte value; + private byte value; /** * Creates the tag.
Boolean true is stored as 1 and boolean false is stored as 0. @@ -58,6 +58,11 @@ public Byte getValue() { return value; } + @Override + public void setValue(Byte value) { + this.value = value; + } + public boolean getBooleanValue() { return value != 0; } diff --git a/src/main/java/com/flowpowered/nbt/CompoundMap.java b/src/main/java/com/flowpowered/nbt/CompoundMap.java index 1124d03..04512d2 100644 --- a/src/main/java/com/flowpowered/nbt/CompoundMap.java +++ b/src/main/java/com/flowpowered/nbt/CompoundMap.java @@ -117,7 +117,7 @@ public CompoundMap(Iterable> initial, boolean sort, boolean reverse) { } } if (initial != null) { - for (Tag t : initial) { + for (Tag t : initial) { put(t); } } @@ -205,8 +205,8 @@ public boolean equals(Object o) { Iterator> iThis = iterator(); Iterator> iOther = other.iterator(); while (iThis.hasNext() && iOther.hasNext()) { - Tag tThis = iThis.next(); - Tag tOther = iOther.next(); + Tag tThis = iThis.next(); + Tag tOther = iOther.next(); if (!tThis.equals(tOther)) { return false; } diff --git a/src/main/java/com/flowpowered/nbt/CompoundTag.java b/src/main/java/com/flowpowered/nbt/CompoundTag.java index 749933e..3cddd51 100644 --- a/src/main/java/com/flowpowered/nbt/CompoundTag.java +++ b/src/main/java/com/flowpowered/nbt/CompoundTag.java @@ -30,7 +30,7 @@ public class CompoundTag extends Tag { /** * The value. */ - private final CompoundMap value; + private CompoundMap value; /** * Creates the tag. @@ -49,6 +49,11 @@ public CompoundMap getValue() { return value; } + @Override + public void setValue(CompoundMap value) { + this.value = value; + } + @Override public String toString() { String name = getName(); @@ -59,14 +64,15 @@ public String toString() { StringBuilder bldr = new StringBuilder(); bldr.append("TAG_Compound").append(append).append(": ").append(value.size()).append(" entries\r\n{\r\n"); - for (Tag entry : value.values()) { + for (Tag entry : value.values()) { bldr.append(" ").append(entry.toString().replaceAll("\r\n", "\r\n ")).append("\r\n"); } bldr.append("}"); return bldr.toString(); } - public CompoundTag clone() { + @Override + public CompoundTag clone() { CompoundMap map = new CompoundMap(value); return new CompoundTag(getName(), map); } diff --git a/src/main/java/com/flowpowered/nbt/DoubleTag.java b/src/main/java/com/flowpowered/nbt/DoubleTag.java index 678982c..d366089 100644 --- a/src/main/java/com/flowpowered/nbt/DoubleTag.java +++ b/src/main/java/com/flowpowered/nbt/DoubleTag.java @@ -30,7 +30,7 @@ public final class DoubleTag extends Tag { /** * The value. */ - private final double value; + private double value; /** * Creates the tag. @@ -48,6 +48,11 @@ public Double getValue() { return value; } + @Override + public void setValue(Double value) { + this.value = value; + } + @Override public String toString() { String name = getName(); diff --git a/src/main/java/com/flowpowered/nbt/EndTag.java b/src/main/java/com/flowpowered/nbt/EndTag.java index fd96593..a3614d5 100644 --- a/src/main/java/com/flowpowered/nbt/EndTag.java +++ b/src/main/java/com/flowpowered/nbt/EndTag.java @@ -39,6 +39,11 @@ public Object getValue() { return null; } + @Override + public void setValue(Object value) { + throw new UnsupportedOperationException(); + } + @Override public String toString() { return "TAG_End"; diff --git a/src/main/java/com/flowpowered/nbt/FloatTag.java b/src/main/java/com/flowpowered/nbt/FloatTag.java index c5b1cbc..1c36784 100644 --- a/src/main/java/com/flowpowered/nbt/FloatTag.java +++ b/src/main/java/com/flowpowered/nbt/FloatTag.java @@ -30,7 +30,7 @@ public final class FloatTag extends Tag { /** * The value. */ - private final float value; + private float value; /** * Creates the tag. @@ -48,6 +48,11 @@ public Float getValue() { return value; } + @Override + public void setValue(Float value) { + this.value = value; + } + @Override public String toString() { String name = getName(); diff --git a/src/main/java/com/flowpowered/nbt/IntArrayTag.java b/src/main/java/com/flowpowered/nbt/IntArrayTag.java index 4fb7fdc..373dccc 100644 --- a/src/main/java/com/flowpowered/nbt/IntArrayTag.java +++ b/src/main/java/com/flowpowered/nbt/IntArrayTag.java @@ -29,7 +29,7 @@ public class IntArrayTag extends Tag { /** * The value. */ - private final int[] value; + private int[] value; /** * Creates the tag. @@ -47,6 +47,11 @@ public int[] getValue() { return value; } + @Override + public void setValue(int[] value) { + this.value = value; + } + @Override public String toString() { StringBuilder hex = new StringBuilder(); diff --git a/src/main/java/com/flowpowered/nbt/IntTag.java b/src/main/java/com/flowpowered/nbt/IntTag.java index fe66bbf..e8d1396 100644 --- a/src/main/java/com/flowpowered/nbt/IntTag.java +++ b/src/main/java/com/flowpowered/nbt/IntTag.java @@ -30,7 +30,7 @@ public final class IntTag extends Tag { /** * The value. */ - private final int value; + private int value; /** * Creates the tag. @@ -48,6 +48,11 @@ public Integer getValue() { return value; } + @Override + public void setValue(Integer value) { + this.value = value; + } + @Override public String toString() { String name = getName(); diff --git a/src/main/java/com/flowpowered/nbt/ListTag.java b/src/main/java/com/flowpowered/nbt/ListTag.java index e2317b4..97ba63e 100644 --- a/src/main/java/com/flowpowered/nbt/ListTag.java +++ b/src/main/java/com/flowpowered/nbt/ListTag.java @@ -38,7 +38,7 @@ public class ListTag> extends Tag> { /** * The value. */ - private final List value; + private List value; /** * Creates the tag. @@ -50,7 +50,7 @@ public class ListTag> extends Tag> { public ListTag(String name, Class type, List value) { super(TagType.TAG_LIST, name); this.type = type; - this.value = Collections.unmodifiableList(value); + this.value = value; } /** @@ -67,6 +67,11 @@ public List getValue() { return value; } + @Override + public void setValue(List value) { + this.value = value; + } + @Override public String toString() { String name = getName(); @@ -77,14 +82,15 @@ public String toString() { StringBuilder bldr = new StringBuilder(); bldr.append("TAG_List").append(append).append(": ").append(value.size()).append(" entries of type ").append(TagType.getByTagClass(type).getTypeName()).append("\r\n{\r\n"); - for (Tag t : value) { + for (Tag t : value) { bldr.append(" ").append(t.toString().replaceAll("\r\n", "\r\n ")).append("\r\n"); } bldr.append("}"); return bldr.toString(); } - @SuppressWarnings ("unchecked") + @Override + @SuppressWarnings ("unchecked") public ListTag clone() { List newList = new ArrayList(); diff --git a/src/main/java/com/flowpowered/nbt/LongArrayTag.java b/src/main/java/com/flowpowered/nbt/LongArrayTag.java new file mode 100644 index 0000000..b0ae06f --- /dev/null +++ b/src/main/java/com/flowpowered/nbt/LongArrayTag.java @@ -0,0 +1,101 @@ +/* + * This file is part of Flow NBT, licensed under the MIT License (MIT). + * + * Copyright (c) 2011 Flow Powered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.flowpowered.nbt; + +import java.util.Arrays; + +public class LongArrayTag extends Tag { + /** + * The value. + */ + private long[] value; + + /** + * Creates the tag. + * + * @param name The name. + * @param value The value. + */ + public LongArrayTag(String name, long[] value) { + super(TagType.TAG_LONG_ARRAY, name); + this.value = value; + } + + @Override + public long[] getValue() { + return value; + } + + @Override + public void setValue(long[] value) { + this.value = value; + } + + @Override + public String toString() { + StringBuilder hex = new StringBuilder(); + for (long s : value) { + String hexDigits = Long.toHexString(s).toUpperCase(); + if (hexDigits.length() == 1) { + hex.append("0"); + } + hex.append(hexDigits).append(" "); + } + + String name = getName(); + String append = ""; + if (name != null && !name.equals("")) { + append = "(\"" + this.getName() + "\")"; + } + return "TAG_Long_Array" + append + ": " + hex.toString(); + } + + @Override + public LongArrayTag clone() { + long[] clonedArray = cloneArray(value); + + return new LongArrayTag(getName(), clonedArray); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof LongArrayTag)) { + return false; + } + + LongArrayTag tag = (LongArrayTag) other; + return Arrays.equals(value, tag.value) && getName().equals(tag.getName()); + } + + private long[] cloneArray(long[] longArray) { + if (longArray == null) { + return null; + } else { + int length = longArray.length; + byte[] newArray = new byte[length]; + System.arraycopy(longArray, 0, newArray, 0, length); + return longArray; + } + } +} diff --git a/src/main/java/com/flowpowered/nbt/LongTag.java b/src/main/java/com/flowpowered/nbt/LongTag.java index d0e023e..dac34a1 100644 --- a/src/main/java/com/flowpowered/nbt/LongTag.java +++ b/src/main/java/com/flowpowered/nbt/LongTag.java @@ -30,7 +30,7 @@ public final class LongTag extends Tag { /** * The value. */ - private final long value; + private long value; /** * Creates the tag. @@ -48,6 +48,11 @@ public Long getValue() { return value; } + @Override + public void setValue(Long value) { + this.value = value; + } + @Override public String toString() { String name = getName(); diff --git a/src/main/java/com/flowpowered/nbt/NBTConstants.java b/src/main/java/com/flowpowered/nbt/NBTConstants.java index 4400597..6903cbd 100644 --- a/src/main/java/com/flowpowered/nbt/NBTConstants.java +++ b/src/main/java/com/flowpowered/nbt/NBTConstants.java @@ -49,6 +49,7 @@ public final class NBTConstants { TYPE_LIST = TagType.TAG_LIST.getId(), TYPE_COMPOUND = TagType.TAG_COMPOUND.getId(), TYPE_INT_ARRAY = TagType.TAG_INT_ARRAY.getId(), + TYPE_LONG_ARRAY = TagType.TAG_LONG_ARRAY.getId(), TYPE_SHORT_ARRAY = TagType.TAG_SHORT_ARRAY.getId(); /** diff --git a/src/main/java/com/flowpowered/nbt/NBTTester.java b/src/main/java/com/flowpowered/nbt/NBTTester.java index 234ca9d..e4a69f8 100644 --- a/src/main/java/com/flowpowered/nbt/NBTTester.java +++ b/src/main/java/com/flowpowered/nbt/NBTTester.java @@ -60,7 +60,7 @@ public static void main(String[] args) { } try { - Tag tag = input.readTag(); + Tag tag = input.readTag(); System.out.println("NBT data from file: " + argFile.getCanonicalPath()); System.out.println(tag); } catch (IOException e) { diff --git a/src/main/java/com/flowpowered/nbt/ShortArrayTag.java b/src/main/java/com/flowpowered/nbt/ShortArrayTag.java index 37f32d9..a8a08f0 100644 --- a/src/main/java/com/flowpowered/nbt/ShortArrayTag.java +++ b/src/main/java/com/flowpowered/nbt/ShortArrayTag.java @@ -29,7 +29,7 @@ public class ShortArrayTag extends Tag { /** * The value. */ - private final short[] value; + private short[] value; /** * Creates the tag. @@ -47,6 +47,11 @@ public short[] getValue() { return value; } + @Override + public void setValue(short[] value) { + this.value = value; + } + @Override public String toString() { StringBuilder hex = new StringBuilder(); diff --git a/src/main/java/com/flowpowered/nbt/ShortTag.java b/src/main/java/com/flowpowered/nbt/ShortTag.java index 11f9e28..22d94fa 100644 --- a/src/main/java/com/flowpowered/nbt/ShortTag.java +++ b/src/main/java/com/flowpowered/nbt/ShortTag.java @@ -30,7 +30,7 @@ public final class ShortTag extends Tag { /** * The value. */ - private final short value; + private short value; /** * Creates the tag. @@ -48,6 +48,11 @@ public Short getValue() { return value; } + @Override + public void setValue(Short value) { + this.value = value; + } + @Override public String toString() { String name = getName(); diff --git a/src/main/java/com/flowpowered/nbt/StringTag.java b/src/main/java/com/flowpowered/nbt/StringTag.java index d974c7a..d8ff32f 100644 --- a/src/main/java/com/flowpowered/nbt/StringTag.java +++ b/src/main/java/com/flowpowered/nbt/StringTag.java @@ -30,7 +30,7 @@ public final class StringTag extends Tag { /** * The value. */ - private final String value; + private String value; /** * Creates the tag. @@ -48,6 +48,11 @@ public String getValue() { return value; } + @Override + public void setValue(String value) { + this.value = value; + } + @Override public String toString() { String name = getName(); diff --git a/src/main/java/com/flowpowered/nbt/Tag.java b/src/main/java/com/flowpowered/nbt/Tag.java index c76cbba..f4fb8cc 100644 --- a/src/main/java/com/flowpowered/nbt/Tag.java +++ b/src/main/java/com/flowpowered/nbt/Tag.java @@ -79,6 +79,13 @@ public TagType getType() { */ public abstract T getValue(); + /** + * Sets the value of this tag + * + * @param value The value of this tag. + */ + public abstract void setValue(T value); + /** * Clones a Map * @@ -107,7 +114,7 @@ public boolean equals(Object other) { } @Override - public int compareTo(Tag other) { + public int compareTo(Tag other) { if (equals(other)) { return 0; } else { @@ -124,5 +131,6 @@ public int compareTo(Tag other) { * * @return the clone */ - public abstract Tag clone(); + @Override + public abstract Tag clone(); } diff --git a/src/main/java/com/flowpowered/nbt/TagType.java b/src/main/java/com/flowpowered/nbt/TagType.java index 96beede..330cdf7 100644 --- a/src/main/java/com/flowpowered/nbt/TagType.java +++ b/src/main/java/com/flowpowered/nbt/TagType.java @@ -40,6 +40,7 @@ public enum TagType { // Java generics, y u so suck TAG_COMPOUND(CompoundTag.class, "TAG_Compound", 10), TAG_INT_ARRAY(IntArrayTag.class, "TAG_Int_Array", 11), + TAG_LONG_ARRAY(LongArrayTag.class, "TAG_Long_Array", 12), TAG_SHORT_ARRAY(ShortArrayTag.class, "TAG_Short_Array", 100),; private static final Map>, TagType> BY_CLASS = new HashMap>, TagType>(); private static final Map BY_NAME = new HashMap(); diff --git a/src/main/java/com/flowpowered/nbt/exception/InvalidTagException.java b/src/main/java/com/flowpowered/nbt/exception/InvalidTagException.java index cfa728e..38a4686 100644 --- a/src/main/java/com/flowpowered/nbt/exception/InvalidTagException.java +++ b/src/main/java/com/flowpowered/nbt/exception/InvalidTagException.java @@ -26,7 +26,10 @@ import com.flowpowered.nbt.Tag; public class InvalidTagException extends Exception { - public InvalidTagException(Tag t) { + + private static final long serialVersionUID = -5446188798223632410L; + + public InvalidTagException(Tag t) { System.out.println("Invalid tag: " + t.toString() + " encountered!"); } } diff --git a/src/main/java/com/flowpowered/nbt/gui/NBTViewer.java b/src/main/java/com/flowpowered/nbt/gui/NBTViewer.java index a3d6ddc..935d081 100644 --- a/src/main/java/com/flowpowered/nbt/gui/NBTViewer.java +++ b/src/main/java/com/flowpowered/nbt/gui/NBTViewer.java @@ -34,6 +34,7 @@ import java.io.InputStream; import java.util.ArrayList; import java.util.List; + import javax.swing.JFrame; import javax.swing.JMenu; import javax.swing.JMenuBar; @@ -99,7 +100,8 @@ public NBTViewer() { public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { - public void run() { + @Override + public void run() { try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch (ClassNotFoundException e) { @@ -141,12 +143,12 @@ private void openFile() { } private List> readFile(File f) { - List> tags = readRawNBT(f, true); + List> tags = readRawNBT(f, NBTInputStream.GZIP_COMPRESSION); if (tags != null) { format = "Compressed NBT"; return tags; } - tags = readRawNBT(f, false); + tags = readRawNBT(f, NBTInputStream.NO_COMPRESSION); if (tags != null) { format = "Uncompressed NBT"; return tags; @@ -166,11 +168,11 @@ private List> readFile(File f) { return null; } - private List> readRawNBT(File f, boolean compressed) { + private List> readRawNBT(File f, int compression) { List> tags = new ArrayList>(); try { InputStream is = new FileInputStream(f); - NBTInputStream ns = new NBTInputStream(is, compressed); + NBTInputStream ns = new NBTInputStream(is, compression); try { boolean eof = false; while (!eof) { diff --git a/src/main/java/com/flowpowered/nbt/holder/FieldHolder.java b/src/main/java/com/flowpowered/nbt/holder/FieldHolder.java index 7d1fa29..9a7e71a 100644 --- a/src/main/java/com/flowpowered/nbt/holder/FieldHolder.java +++ b/src/main/java/com/flowpowered/nbt/holder/FieldHolder.java @@ -67,22 +67,44 @@ public void load(CompoundTag tag) { } } + @Deprecated public void save(File file, boolean compressed) throws IOException { save(new FileOutputStream(file), compressed); } + public void save(File file, int compression) throws IOException { + save(new FileOutputStream(file), compression); + } + + @Deprecated public void save(OutputStream stream, boolean compressed) throws IOException { - NBTOutputStream os = new NBTOutputStream(stream, compressed); + save(stream, compressed ? NBTInputStream.GZIP_COMPRESSION : NBTInputStream.NO_COMPRESSION); + } + + public void save(OutputStream stream, int compression) throws IOException { + NBTOutputStream os = new NBTOutputStream(stream, compression); os.writeTag(new CompoundTag("", save())); + os.close(); } + @Deprecated public void load(File file, boolean compressed) throws IOException { load(new FileInputStream(file), compressed); } + public void load(File file, int compression) throws IOException { + load(new FileInputStream(file), compression); + } + + @Deprecated public void load(InputStream stream, boolean compressed) throws IOException { - NBTInputStream is = new NBTInputStream(stream, compressed); + load(stream, compressed ? NBTInputStream.GZIP_COMPRESSION : NBTInputStream.NO_COMPRESSION); + } + + public void load(InputStream stream, int compression) throws IOException { + NBTInputStream is = new NBTInputStream(stream, compression); Tag tag = is.readTag(); + is.close(); if (!(tag instanceof CompoundTag)) { throw new IllegalArgumentException("Expected CompoundTag, got " + tag.getClass()); } diff --git a/src/main/java/com/flowpowered/nbt/holder/FieldValue.java b/src/main/java/com/flowpowered/nbt/holder/FieldValue.java index 3537ba8..d7cc875 100644 --- a/src/main/java/com/flowpowered/nbt/holder/FieldValue.java +++ b/src/main/java/com/flowpowered/nbt/holder/FieldValue.java @@ -53,7 +53,7 @@ public FieldValue(String key, Field field, T defaultValue) { * @return The value */ public T load(CompoundTag tag) { - Tag subTag = tag.getValue().get(key); + Tag subTag = tag.getValue().get(key); if (subTag == null) { return (value = defaultValue); } @@ -67,7 +67,7 @@ public void save(CompoundMap tag) { return; } } - Tag t = field.getValue(key, value); + Tag t = field.getValue(key, value); tag.put(t); } diff --git a/src/main/java/com/flowpowered/nbt/itemmap/StringMapReader.java b/src/main/java/com/flowpowered/nbt/itemmap/StringMapReader.java index 117f528..f069024 100644 --- a/src/main/java/com/flowpowered/nbt/itemmap/StringMapReader.java +++ b/src/main/java/com/flowpowered/nbt/itemmap/StringMapReader.java @@ -38,9 +38,8 @@ public class StringMapReader { public static List> readFile(File f) { List> list = new ArrayList>(); - try { - FileInputStream fis = new FileInputStream(f); - DataInputStream dis = new DataInputStream(fis); + try (FileInputStream fis = new FileInputStream(f); + DataInputStream dis = new DataInputStream(fis);){ boolean eof = false; while (!eof) { int value; diff --git a/src/main/java/com/flowpowered/nbt/regionfile/Chunk.java b/src/main/java/com/flowpowered/nbt/regionfile/Chunk.java new file mode 100644 index 0000000..5fa4800 --- /dev/null +++ b/src/main/java/com/flowpowered/nbt/regionfile/Chunk.java @@ -0,0 +1,298 @@ +package com.flowpowered.nbt.regionfile; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import com.flowpowered.nbt.CompoundMap; +import com.flowpowered.nbt.CompoundTag; +import com.flowpowered.nbt.DoubleTag; +import com.flowpowered.nbt.IntTag; +import com.flowpowered.nbt.ListTag; +import com.flowpowered.nbt.Tag; +import com.flowpowered.nbt.stream.NBTInputStream; +import com.flowpowered.nbt.stream.NBTOutputStream; + +/** + * Each instance of this class represents a Minecraft chunk within a Region file. The data is represented as a binary blob in form of a + * {@link ByteBuffer}. Each object is self-contained and not linked to any physical file or other object. Each object is immutable and + * should be treated as such. The data is set in the constructor
+ * The data must have a length of a multiple of 4096 bytes (to be able to write it to disk more easily). The four first bytes specify the + * amount of actual data bytes in the buffer following. The fifth byte contains the compression method. All following bytes are NBT data. + * + * @author piegames + */ +public class Chunk { + /** The x coordinate of the chunk relative to its RegionFile's origin */ + public final int x; + /** The z coordinate of the chunk relative to its RegionFile's origin */ + public final int z; + /** + * The point in time when this chunk last got written. Equal to {@code (int) (System.currentTimeMillis() / 1000L)} + */ + public final int timestamp; + + protected final ByteBuffer data; + + /** + * Create a Chunk object by setting its data directly through a buffer. + * + * @param x + * The x coordinate of the chunk within its region file. Must be in the range of [0..32) because region files are always 32*32 + * chunks large. + * @param z + * The z coordinate of the chunk within its region file. Must be in the range of [0..32) because region files are always 32*32 + * chunks large. + * @param data + * The data of the chunk + * @throws IllegalArgumentException + * if the coordinates are out of bounds or the data does not have a size multiple of 4096 + * @author piegames + */ + public Chunk(int x, int z, int timestamp, ByteBuffer data) { + if (x < 0 || z < 0 || x >= 32 || z >= 32) + throw new IllegalArgumentException("Coordinates must be in range [0..32), but were x=" + x + ", z=" + z + ")"); + this.x = x; + this.z = z; + this.timestamp = timestamp; + + if ((data.capacity() & 4095) != 0) + throw new IllegalArgumentException("Data buffer size must be multiple of 4096, but is " + data.capacity()); + this.data = Objects.requireNonNull(data); + } + + /** + * Create a Chunk object by reading specified data from a region file (*.mca, *.mcr). + * + * @param x + * The x coordinate of the chunk within its region file. Must be in the range of [0..32) because region files are always 32*32 + * chunks large. + * @param z + * The z coordinate of the chunk within its region file. Must be in the range of [0..32) because region files are always 32*32 + * chunks large. + * @param raf + * The file channel to the region file from which to load the data + * @param start + * The number of the 4096 byte sector where the chunk is located in the file. Don't forget that the first five bytes are used to + * store the size and compression of the chunk. The position of the first byte of NBT data is thus {@code start*4096 + 5}. + * @param length + * The amount of 4096 byte sectors to load. It should be large enough to contain all NBT data in that chunk or it will be + * corrupted. + * @throws IllegalArgumentException + * if the coordinates are out of bounds + * @author piegames + */ + Chunk(int x, int z, int timestamp, FileChannel raf, int start, int length) throws IOException { + if (x < 0 || z < 0 || x >= 32 || z >= 32) + throw new IllegalArgumentException("Coordinates must be in range [0..32), but were x=" + x + ", z=" + z + ")"); + this.x = x; + this.z = z; + this.timestamp = timestamp; + + data = ByteBuffer.allocate(4096 * length); + raf.read(data, 4096 * start); + data.flip(); + } + + /** + * Create a Chunk object by filling the NBT tag's data to a {@link ByteBuffer} using the specified compression method. + * + * @param x + * The x coordinate of the chunk within its region file. Must be in the range of [0..32) because region files are always 32*32 + * chunks large. + * @param z + * The z coordinate of the chunk within its region file. Must be in the range of [0..32) because region files are always 32*32 + * chunks large. + * @param data + * The NBT data the chunk will contain. Should be a {@link CompoundTag} + * @param compression + * The compression to use + * @throws IllegalArgumentException + * if the coordinates are out of bounds + * @author piegames + */ + public Chunk(int x, int z, int timestamp, Tag data, byte compression) throws IOException { + if (x < 0 || z < 0 || x >= 32 || z >= 32) + throw new IllegalArgumentException("Coordinates must be in range [0..32), but were x=" + x + ", z=" + z + ")"); + this.x = x; + this.z = z; + this.timestamp = timestamp; + + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(4096); + NBTOutputStream out = new NBTOutputStream(baos, compression)) { + out.writeTag(data); + out.flush(); + out.close(); + + byte[] bytes = baos.toByteArray(); + int sectionLength = (bytes.length + 5) / 4096 + 1; + this.data = ByteBuffer.allocate(sectionLength * 4096); + this.data.putInt(bytes.length + 1); + this.data.put(compression); + this.data.put(bytes); + this.data.flip(); + } + } + + /** + * Create a Chunk object identical to an existing one, except for a different timestamp. + * + * @param data + * The chunk to clone + * @author piegames + */ + public Chunk(Chunk data, int timestamp) { + this.x = data.x; + this.z = data.z; + this.timestamp = timestamp; + + this.data = data.data; + } + + /** + * Returns the compression method used in this chunk as specified by the format. This value corresponds to the compression that an + * {@link NBTInputStream} takes in its constructor. + */ + public byte getCompression() { + return data.get(4); + } + + /** + * The real length of the NBT data in this chunk in bytes. + */ + public int getRealLength() { + return data.getInt(0) - 1; + } + + /** + * Get the amount of 4kiB-sized sectors on the hard disk that would be required to save this chunk. + */ + public int getSectorLength() { + return (getRealLength() + 5) / 4096 + 1; + } + + /** + * Returns the {@link ByteBuffer} containing all the data in this chunk, including the five bytes before the actual NBT data. It will always + * contain a multiple of 4096 bytes. Altering its content will result in undefined behavior! + */ + public ByteBuffer getData() { + return data; + } + + /** + * Open an {@link NBTInputStream} for reading the NBT data contained in that chunk. + */ + public NBTInputStream getInputStream() throws IOException { + return new NBTInputStream(new ByteArrayInputStream(data.array(), 5, getRealLength()), getCompression()); + } + + /** + * Reads the NBT chunk data and returns it. The normally nameless root tag will be renamed to "chunk". + */ + public CompoundTag readTag() throws IOException { + try (NBTInputStream nbtIn = getInputStream();) { + return new CompoundTag("chunk", ((CompoundTag) nbtIn.readTag()).getValue()); + } + } + + /** + * Return a timestamp in the format used by Minecraft representing the point in time this method was called + * + * @see #timestamp + */ + public static int getCurrentTimestamp() { + return (int) (System.currentTimeMillis() / 1000L); + } + + public static int bitsPerIndex(long[] blocks) { + /* There are {@code 16*16*16=4096} blocks in each chunk, and a long has 64 bits */ + return blocks.length * 64 / 4096; + } + + /** + * Extract a palette index from the long array. This data is located at {@code /Level/Sections[i]/BlockStates}. + * + * @param blocks + * a long array containing all the block states as Minecraft encodes them to {@code /Level/Sections[i]/BlockStates} within each + * section of a chunk. + * @param i + * The index of the block to be extracted. Since the data is mapped XZY, {@code i = x | (z<<4) | (y<<8)}. + * @param bitsPerIndex + * The amount of bits each index has. This is to avoid redundant calculation on each call. + * + * @see #bitsPerIndex(long[]) + * @author piegames + */ + public static long extractFromLong(long[] blocks, int i, int bitsPerIndex) { + int startByte = (bitsPerIndex * i) >> 6; // >> 6 equals / 64 + int endByte = (bitsPerIndex * (i + 1)) >> 6; + // The bit within the long where our value starts. Counting from the right LSB (!). + int startByteBit = ((bitsPerIndex * i)) & 63; // % 64 equals & 63 + int endByteBit = ((bitsPerIndex * (i + 1))) & 63; + + // Use bit shifting and & bit masking to extract bit sequences out of longs as numbers + // -1L is the value with every bit set + long blockIndex; + if (startByte == endByte) { + // Normal case: the bit string we need is within a single long + blockIndex = (blocks[startByte] << (64 - endByteBit)) >>> (64 + startByteBit - endByteBit); + } else if (endByteBit == 0) { + // The bit string is exactly at the beginning of a long + blockIndex = blocks[startByte] >>> startByteBit; + } else { + // The bit string is overlapping two longs + blockIndex = ((blocks[startByte] >>> startByteBit)) + | ((blocks[endByte] << (64 - endByteBit)) >>> (startByteBit - endByteBit)); + } + return blockIndex; + } + + /** Incubating utility method, use with care */ + @SuppressWarnings("unchecked") + public static void moveChunk(CompoundTag level, int sourceX, int sourceZ, int destX, int destZ) { + CompoundMap value = level.getValue(); + /* The difference in blocks between the two chunks */ + int diffX = (destX - sourceX) << 4; + int diffY = 0; + int diffZ = (destZ - sourceZ) << 4; + + value.put(new IntTag("xPos", destX)); + value.put(new IntTag("zPos", destZ)); + + /* Update entities */ + for (CompoundTag entity : ((ListTag) value.get("Entities")).getValue()) { + List pos = ((ListTag) entity.getValue().get("Pos")).getValue(); + entity.getValue().put(new ListTag<>("Pos", DoubleTag.class, + Arrays.asList( + new DoubleTag(null, pos.get(0).getValue() + diffX), + new DoubleTag(null, pos.get(1).getValue() + diffY), + new DoubleTag(null, pos.get(2).getValue() + diffZ)))); + } + /* Update tile entities */ + for (CompoundTag tileEntity : ((ListTag) value.get("TileEntities")).getValue()) { + CompoundMap map = tileEntity.getValue(); + map.put(new IntTag("x", ((IntTag) map.get("x")).getValue() + diffX)); + map.put(new IntTag("y", ((IntTag) map.get("y")).getValue() + diffY)); + map.put(new IntTag("z", ((IntTag) map.get("z")).getValue() + diffZ)); + } + /* Update tile ticks */ + for (CompoundTag tileTick : ((ListTag) value.get("TileTicks")).getValue()) { + CompoundMap map = tileTick.getValue(); + map.put(new IntTag("x", ((IntTag) map.get("x")).getValue() + diffX)); + map.put(new IntTag("y", ((IntTag) map.get("y")).getValue() + diffY)); + map.put(new IntTag("z", ((IntTag) map.get("z")).getValue() + diffZ)); + } + /* Update liquid ticks */ + for (CompoundTag liquidTick : ((ListTag) value.get("LiquidTicks")).getValue()) { + CompoundMap map = liquidTick.getValue(); + map.put(new IntTag("x", ((IntTag) map.get("x")).getValue() + diffX)); + map.put(new IntTag("y", ((IntTag) map.get("y")).getValue() + diffY)); + map.put(new IntTag("z", ((IntTag) map.get("z")).getValue() + diffZ)); + } + } +} diff --git a/src/main/java/com/flowpowered/nbt/regionfile/RegionFile.java b/src/main/java/com/flowpowered/nbt/regionfile/RegionFile.java new file mode 100644 index 0000000..70011ec --- /dev/null +++ b/src/main/java/com/flowpowered/nbt/regionfile/RegionFile.java @@ -0,0 +1,242 @@ +package com.flowpowered.nbt.regionfile; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.IntBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.BitSet; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +/** + * This helper class provides functionality to read the data of single chunks in a region/anvil file. It uses modern {@code java.nio} + * classes like {@link Path} and {@link FileChannel} to access its data from the file. Each instance of the class represents a single file, + * whose header will be loaded in the constructor. + * + * @author piegames + */ +public class RegionFile implements Closeable { + + protected final Path file; + protected FileChannel raf; + + protected ByteBuffer locations; + protected IntBuffer locations2; + protected ByteBuffer timestamps; + protected IntBuffer timestamps2; + + /** + * Create a new RegionFile object representing the region file at the given path and load it's header to memory. + * + * @throws IllegalArgumentException + * if the file is smaller than 4kiB + * @throws NoSuchFileException + * if the file does not exist + * @see #open(Path) + * @author piegames + */ + public RegionFile(Path file) throws IOException { + this.file = Objects.requireNonNull(file); + + if (!Files.exists(file)) + throw new NoSuchFileException(file.toString()); + if (Files.size(file) < 4096 * 2) + throw new IllegalArgumentException("File size must be at least 4kiB, is this file corrupt?"); + raf = FileChannel.open(file, StandardOpenOption.READ, StandardOpenOption.WRITE); + + locations = ByteBuffer.allocate(4096); + raf.read(locations); + locations.flip(); + locations2 = locations.asIntBuffer(); + + timestamps = ByteBuffer.allocate(4096); + raf.read(timestamps); + timestamps.flip(); + timestamps2 = timestamps.asIntBuffer(); + } + + /** + * Load the {@link Chunk} at the given coordinate + * + * @see #coordsToPosition(int, int) + * @return the chunk at that coordinate or {@code null} if the chunk does not exist + * @throws IOException + * @author piegames + */ + public Chunk loadChunk(int x, int z) throws IOException { + return loadChunk(coordsToPosition(x, z)); + } + + /** @see #loadChunk(int, int) */ + public Chunk loadChunk(int i) throws IOException { + int chunkPos = locations2.get(i) >>> 8; + int chunkLength = locations2.get(i) & 0xFF; + if (chunkPos > 0) { + /* i & 31 retrieves the last 5 bit which store the x coordinate */ + return new Chunk(i & 31, i >> 5, timestamps2.get(i), raf, chunkPos, chunkLength); + } + return null; + } + + /** + * Tell if the file contains a chunk at this position. + * + * @return {@code true} if there is a chunk at this position + * @see #coordsToPosition(int, int) + */ + public boolean hasChunk(int x, int z) { + return hasChunk((x & 31) | (z << 5)); + } + + /** @see #hasChunk(int, int) */ + public boolean hasChunk(int i) { + return (locations2.get(i) >>> 8) > 0; + } + + /** + * Same as {@link #listChunks()}, but as stream. + */ + public Stream streamChunks() { + return IntStream.range(0, 32 * 32).filter(pos -> hasChunk(pos)) + .boxed() + .sorted(Comparator.comparingInt(i -> locations2.get(i) >>> 8)); + } + + /** + * List the positions of all chunks that exist in this file sorted by their their appearance order in the file. Use this to read all chunks + * in their sequential order to speed up seek times. + * + * @see #coordsToPosition(int, int) + */ + public List listChunks() { + return streamChunks().collect(Collectors.toList()); + } + + /** + * Write all given chunks to disk, update the file's header (chunk locations and timestamps) and truncate the file at the end + * + * @param changedChunks + * {@link HashMap} of all changes to write. Each key is the position of one changed chunk (use + * {@link #coordsToPosition(int, int)} to calculate the key from a coordinate). The map may contain {@code null} values, + * indicating that the chunk should be removed from the file. + * @author piegames + */ + public void writeChunks(HashMap changedChunks) throws IOException { + synchronized (raf) { + /* Mark all 4kib sectors in the file if they are used. */ + BitSet usedSectors = new BitSet(); + /* Set the first two sectors as used since they always are (by the header) */ + usedSectors.set(0, 2); + + /* Mark the currently used sectors, but omit those that are going to be deleted or overwritten. */ + for (int i = 0; i < 32 * 32; i++) { + int chunkPos = locations2.get(i) >>> 8; + int chunkLength = locations2.get(i) & 0xFF; + if (chunkLength > 0 && !changedChunks.containsKey(i)) + usedSectors.set(chunkPos, chunkPos + chunkLength); + } + + /* Iterate through all changed chunks and try to fit them in somewhere */ + for (Integer chunkPos : changedChunks.keySet()) { + Chunk chunk = changedChunks.get(chunkPos); + if (chunk == null) { + /* Position zero, length zero */ + locations2.put(chunkPos, 0); + } else { + int length = 0; + int start = 0; + /* Increase start until we found a solid place to put our data */ + while (length < chunk.getSectorLength()) { + if (!usedSectors.get(start + length)) { + length++; + } else { + start = usedSectors.nextClearBit(start + length); + length = 0; + } + } + if (length > 255) + throw new IOException("Chunks are limited to a length of maximum 255 sectors, or ~1MiB"); + { /* Write the chunk to disk */ + raf.position(start * 4096); + raf.write(chunk.data); + timestamps2.put(chunkPos, chunk.timestamp); + } + locations2.put(chunkPos, start << 8 | length); + usedSectors.set(start, start + length); + } + } + /* Write updated header */ + raf.position(0); + raf.write(locations); + raf.write(timestamps); + locations.flip(); + timestamps.flip(); + + raf.truncate(4096 * usedSectors.previousSetBit(usedSectors.size()) + 4096); + } + changedChunks.clear(); + } + + /** Get the path this file is associated with. It will never change over time. */ + public Path getPath() { + return file; + } + + @Override + public void close() throws IOException { + raf.close(); + } + + /** + * Create a new region file by writing an empty header to it. + * + * @author piegames + */ + public static RegionFile createNew(Path file) throws IOException { + try (FileChannel raf = FileChannel.open(file, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);) { + /* Write empty header */ + raf.write(ByteBuffer.wrap(new byte[2 * 4096])); + } + return new RegionFile(file); + } + + /** + * Open an existing region file, creating it if it does not exist + * + * @author piegames + */ + public static RegionFile open(Path file) throws IOException { + if (Files.exists(file)) + return new RegionFile(file); + else { + Files.createDirectories(file.getParent()); + return createNew(file); + } + } + + /** + * Convert a coordinate into a position index. + * + * @param x + * The x position of the chunk in chunk coordinates (1 unit <=> 16 blocks). The coordinate should be relative to the region + * file's position, but using the world's origin works fine as well. + * @param z + * The z position of the chunk in chunk coordinates (1 unit <=> 16 blocks). The coordinate should be relative to the region + * file's position, but using the world's origin works fine as well. + * @return The index of this chunk in the file. This is a number between 0 (inclusive) and 32*32 (exclusive). The coordinate is flattened in + * x-z-order. + */ + public static int coordsToPosition(int x, int z) { + return (x & 31) | ((z & 31) << 5); + } +} \ No newline at end of file diff --git a/src/main/java/com/flowpowered/nbt/regionfile/SimpleRegionFileReader.java b/src/main/java/com/flowpowered/nbt/regionfile/SimpleRegionFileReader.java index 068980b..90bcebd 100644 --- a/src/main/java/com/flowpowered/nbt/regionfile/SimpleRegionFileReader.java +++ b/src/main/java/com/flowpowered/nbt/regionfile/SimpleRegionFileReader.java @@ -23,81 +23,33 @@ */ package com.flowpowered.nbt.regionfile; -import java.io.ByteArrayInputStream; import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; -import java.io.RandomAccessFile; -import java.util.ArrayList; import java.util.List; -import java.util.zip.InflaterInputStream; +import java.util.Objects; +import java.util.stream.Collectors; import com.flowpowered.nbt.Tag; -import com.flowpowered.nbt.stream.NBTInputStream; +@Deprecated public class SimpleRegionFileReader { - private static int EXPECTED_VERSION = 1; - public static List> readFile(File f) { - RandomAccessFile raf; - try { - raf = new RandomAccessFile(f, "r"); - } catch (FileNotFoundException e) { - return null; - } - - try { - int version = raf.readInt(); - - if (version != EXPECTED_VERSION) { - return null; - } - - int segmentSize = raf.readInt(); - int segmentMask = (1 << segmentSize) - 1; - int entries = raf.readInt(); - - List> list = new ArrayList>(entries); - - int[] blockSegmentStart = new int[entries]; - int[] blockActualLength = new int[entries]; - - for (int i = 0; i < entries; i++) { - blockSegmentStart[i] = raf.readInt(); - blockActualLength[i] = raf.readInt(); - } - - for (int i = 0; i < entries; i++) { - if (blockActualLength[i] == 0) { - list.add(null); - continue; - } - byte[] data = new byte[blockActualLength[i]]; - raf.seek(blockSegmentStart[i] << segmentSize); - raf.readFully(data); - ByteArrayInputStream in = new ByteArrayInputStream(data); - InflaterInputStream iis = new InflaterInputStream(in); - NBTInputStream ns = new NBTInputStream(iis, false); - try { - Tag t = ns.readTag(); - list.add(t); - } catch (IOException ioe) { - list.add(null); - } - try { - ns.close(); - } catch (IOException ioe) { - } - } - - return list; - } catch (IOException ioe) { - return null; - } finally { - try { - raf.close(); - } catch (IOException ioe) { - } - } - } + /** @deprecated Legacy crap. Use {@link RegionFile} directly instead. */ + @Deprecated + public static List> readFile(File f) { + try (RegionFile file = new RegionFile(f.toPath())) { + return file.streamChunks() + .map(i -> { + try { + return file.loadChunk(i).readTag(); + } catch (IOException e) { + return null; + } + }) + .filter(Objects::isNull) + .collect(Collectors.toList()); + } catch (IOException e1) { + return null; + } + } } diff --git a/src/main/java/com/flowpowered/nbt/stream/EndianSwitchableInputStream.java b/src/main/java/com/flowpowered/nbt/stream/EndianSwitchableInputStream.java deleted file mode 100644 index f6c900f..0000000 --- a/src/main/java/com/flowpowered/nbt/stream/EndianSwitchableInputStream.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * This file is part of Flow NBT, licensed under the MIT License (MIT). - * - * Copyright (c) 2011 Flow Powered - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package com.flowpowered.nbt.stream; - -import java.io.DataInput; -import java.io.DataInputStream; -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteOrder; - -/** - * A wrapper around {@link DataInputStream} that allows changing the endianness of data. By default, everything in Java is big-endian - */ -public class EndianSwitchableInputStream extends FilterInputStream implements DataInput { - private final ByteOrder endianness; - - public EndianSwitchableInputStream(InputStream stream, ByteOrder endianness) { - super(stream instanceof DataInputStream ? stream : new DataInputStream(stream)); - this.endianness = endianness; - } - - public ByteOrder getEndianness() { - return endianness; - } - - protected DataInputStream getBackingStream() { - return (DataInputStream) super.in; - } - - public void readFully(byte[] bytes) throws IOException { - getBackingStream().readFully(bytes); - } - - public void readFully(byte[] bytes, int i, int i1) throws IOException { - getBackingStream().readFully(bytes, i, i1); - } - - public int skipBytes(int i) throws IOException { - return getBackingStream().skipBytes(i); - } - - public boolean readBoolean() throws IOException { - return getBackingStream().readBoolean(); - } - - public byte readByte() throws IOException { - return getBackingStream().readByte(); - } - - public int readUnsignedByte() throws IOException { - return getBackingStream().readUnsignedByte(); - } - - public short readShort() throws IOException { - short ret = getBackingStream().readShort(); - if (endianness == ByteOrder.LITTLE_ENDIAN) { - ret = Short.reverseBytes(ret); - } - return ret; - } - - public int readUnsignedShort() throws IOException { - int ret = getBackingStream().readUnsignedShort(); - if (endianness == ByteOrder.LITTLE_ENDIAN) { - ret = (char) (Integer.reverseBytes(ret) >> 16); - } - return ret; - } - - public char readChar() throws IOException { - char ret = getBackingStream().readChar(); - if (endianness == ByteOrder.LITTLE_ENDIAN) { - ret = Character.reverseBytes(ret); - } - return ret; - } - - public int readInt() throws IOException { - return endianness == ByteOrder.LITTLE_ENDIAN ? Integer.reverseBytes(getBackingStream().readInt()) : getBackingStream().readInt(); - } - - public long readLong() throws IOException { - return endianness == ByteOrder.LITTLE_ENDIAN ? Long.reverseBytes(getBackingStream().readLong()) : getBackingStream().readLong(); - } - - public float readFloat() throws IOException { - int result = readInt(); - if (endianness == ByteOrder.LITTLE_ENDIAN) { - result = Integer.reverseBytes(result); - } - return Float.intBitsToFloat(result); - } - - public double readDouble() throws IOException { - long result = readLong(); - if (endianness == ByteOrder.LITTLE_ENDIAN) { - result = Long.reverseBytes(result); - } - return Double.longBitsToDouble(result); - } - - @SuppressWarnings ("deprecation") // This method is deprecated - public String readLine() throws IOException { - return getBackingStream().readLine(); - } - - public String readUTF() throws IOException { - return getBackingStream().readUTF(); - } -} diff --git a/src/main/java/com/flowpowered/nbt/stream/EndianSwitchableOutputStream.java b/src/main/java/com/flowpowered/nbt/stream/EndianSwitchableOutputStream.java deleted file mode 100644 index a64de79..0000000 --- a/src/main/java/com/flowpowered/nbt/stream/EndianSwitchableOutputStream.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * This file is part of Flow NBT, licensed under the MIT License (MIT). - * - * Copyright (c) 2011 Flow Powered - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ -package com.flowpowered.nbt.stream; - -import java.io.DataOutput; -import java.io.DataOutputStream; -import java.io.FilterOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.ByteOrder; - -public class EndianSwitchableOutputStream extends FilterOutputStream implements DataOutput { - private final ByteOrder endianness; - - public EndianSwitchableOutputStream(OutputStream backingStream, ByteOrder endianness) { - super(backingStream instanceof DataOutputStream ? (DataOutputStream) backingStream : new DataOutputStream(backingStream)); - this.endianness = endianness; - } - - public ByteOrder getEndianness() { - return endianness; - } - - protected DataOutputStream getBackingStream() { - return (DataOutputStream) super.out; - } - - public void writeBoolean(boolean b) throws IOException { - getBackingStream().writeBoolean(b); - } - - public void writeByte(int i) throws IOException { - getBackingStream().writeByte(i); - } - - public void writeShort(int i) throws IOException { - if (endianness == ByteOrder.LITTLE_ENDIAN) { - i = Integer.reverseBytes(i) >> 16; - } - getBackingStream().writeShort(i); - } - - public void writeChar(int i) throws IOException { - if (endianness == ByteOrder.LITTLE_ENDIAN) { - i = Character.reverseBytes((char) i); - } - getBackingStream().writeChar(i); - } - - public void writeInt(int i) throws IOException { - if (endianness == ByteOrder.LITTLE_ENDIAN) { - i = Integer.reverseBytes(i); - } - getBackingStream().writeInt(i); - } - - public void writeLong(long l) throws IOException { - if (endianness == ByteOrder.LITTLE_ENDIAN) { - l = Long.reverseBytes(l); - } - getBackingStream().writeLong(l); - } - - public void writeFloat(float v) throws IOException { - int intBits = Float.floatToIntBits(v); - if (endianness == ByteOrder.LITTLE_ENDIAN) { - intBits = Integer.reverseBytes(intBits); - } - getBackingStream().writeInt(intBits); - } - - public void writeDouble(double v) throws IOException { - long longBits = Double.doubleToLongBits(v); - if (endianness == ByteOrder.LITTLE_ENDIAN) { - longBits = Long.reverseBytes(longBits); - } - getBackingStream().writeLong(longBits); - } - - public void writeBytes(String s) throws IOException { - getBackingStream().writeBytes(s); - } - - public void writeChars(String s) throws IOException { - getBackingStream().writeChars(s); - } - - public void writeUTF(String s) throws IOException { - getBackingStream().writeUTF(s); - } -} diff --git a/src/main/java/com/flowpowered/nbt/stream/LittleEndianInputStream.java b/src/main/java/com/flowpowered/nbt/stream/LittleEndianInputStream.java new file mode 100644 index 0000000..33cc426 --- /dev/null +++ b/src/main/java/com/flowpowered/nbt/stream/LittleEndianInputStream.java @@ -0,0 +1,126 @@ +/* + * This file is part of Flow NBT, licensed under the MIT License (MIT). + * + * Copyright (c) 2011 Flow Powered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.flowpowered.nbt.stream; + +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteOrder; + +/** + * A wrapper around {@link DataInputStream} that allows changing the endianness of data. By default, everything in Java is big-endian + */ +public class LittleEndianInputStream extends FilterInputStream implements DataInput { + + public LittleEndianInputStream(InputStream stream) { + super(stream instanceof DataInputStream ? stream : new DataInputStream(stream)); + } + + @Deprecated + public ByteOrder getEndianness() { + return ByteOrder.LITTLE_ENDIAN; + } + + protected DataInputStream getBackingStream() { + return (DataInputStream) super.in; + } + + @Override + public void readFully(byte[] bytes) throws IOException { + getBackingStream().readFully(bytes); + } + + @Override + public void readFully(byte[] bytes, int i, int i1) throws IOException { + getBackingStream().readFully(bytes, i, i1); + } + + @Override + public int skipBytes(int i) throws IOException { + return getBackingStream().skipBytes(i); + } + + @Override + public boolean readBoolean() throws IOException { + return getBackingStream().readBoolean(); + } + + @Override + public byte readByte() throws IOException { + return getBackingStream().readByte(); + } + + @Override + public int readUnsignedByte() throws IOException { + return getBackingStream().readUnsignedByte(); + } + + @Override + public short readShort() throws IOException { + return Short.reverseBytes(getBackingStream().readShort()); + } + + @Override + public int readUnsignedShort() throws IOException { + return (char) (Integer.reverseBytes(getBackingStream().readUnsignedShort()) >> 16); + } + + @Override + public char readChar() throws IOException { + return Character.reverseBytes(getBackingStream().readChar()); + } + + @Override + public int readInt() throws IOException { + return Integer.reverseBytes(getBackingStream().readInt()); + } + + @Override + public long readLong() throws IOException { + return Long.reverseBytes(getBackingStream().readLong()); + } + + @Override + public float readFloat() throws IOException { + return Float.intBitsToFloat(Integer.reverseBytes(readInt())); + } + + @Override + public double readDouble() throws IOException { + return Double.longBitsToDouble(Long.reverseBytes(readLong())); + } + + @Override + @SuppressWarnings("deprecation") // This method is deprecated + public String readLine() throws IOException { + return getBackingStream().readLine(); + } + + @Override + public String readUTF() throws IOException { + return getBackingStream().readUTF(); + } +} diff --git a/src/main/java/com/flowpowered/nbt/stream/LittleEndianOutputStream.java b/src/main/java/com/flowpowered/nbt/stream/LittleEndianOutputStream.java new file mode 100644 index 0000000..d84d06c --- /dev/null +++ b/src/main/java/com/flowpowered/nbt/stream/LittleEndianOutputStream.java @@ -0,0 +1,102 @@ +/* + * This file is part of Flow NBT, licensed under the MIT License (MIT). + * + * Copyright (c) 2011 Flow Powered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package com.flowpowered.nbt.stream; + +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteOrder; + +public class LittleEndianOutputStream extends FilterOutputStream implements DataOutput { + + public LittleEndianOutputStream(OutputStream backingStream) { + super(backingStream instanceof DataOutputStream ? (DataOutputStream) backingStream : new DataOutputStream(backingStream)); + } + + @Deprecated + public ByteOrder getEndianness() { + return ByteOrder.LITTLE_ENDIAN; + } + + protected DataOutputStream getBackingStream() { + return (DataOutputStream) super.out; + } + + @Override + public void writeBoolean(boolean b) throws IOException { + getBackingStream().writeBoolean(b); + } + + @Override + public void writeByte(int i) throws IOException { + getBackingStream().writeByte(i); + } + + @Override + public void writeShort(int i) throws IOException { + getBackingStream().writeShort(Integer.reverseBytes(i) >> 16); + } + + @Override + public void writeChar(int i) throws IOException { + getBackingStream().writeChar(Character.reverseBytes((char) i)); + } + + @Override + public void writeInt(int i) throws IOException { + getBackingStream().writeInt(Integer.reverseBytes(i)); + } + + @Override + public void writeLong(long l) throws IOException { + getBackingStream().writeLong(Long.reverseBytes(l)); + } + + @Override + public void writeFloat(float v) throws IOException { + getBackingStream().writeInt(Integer.reverseBytes(Float.floatToIntBits(v))); + } + + @Override + public void writeDouble(double v) throws IOException { + getBackingStream().writeLong(Long.reverseBytes(Double.doubleToLongBits(v))); + } + + @Override + public void writeBytes(String s) throws IOException { + getBackingStream().writeBytes(s); + } + + @Override + public void writeChars(String s) throws IOException { + getBackingStream().writeChars(s); + } + + @Override + public void writeUTF(String s) throws IOException { + getBackingStream().writeUTF(s); + } +} diff --git a/src/main/java/com/flowpowered/nbt/stream/NBTInputStream.java b/src/main/java/com/flowpowered/nbt/stream/NBTInputStream.java index c87515b..d717c86 100644 --- a/src/main/java/com/flowpowered/nbt/stream/NBTInputStream.java +++ b/src/main/java/com/flowpowered/nbt/stream/NBTInputStream.java @@ -24,218 +24,303 @@ package com.flowpowered.nbt.stream; import java.io.Closeable; +import java.io.DataInput; +import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.List; import java.util.zip.GZIPInputStream; +import java.util.zip.InflaterInputStream; -import com.flowpowered.nbt.ByteArrayTag; -import com.flowpowered.nbt.ByteTag; -import com.flowpowered.nbt.CompoundMap; -import com.flowpowered.nbt.CompoundTag; -import com.flowpowered.nbt.DoubleTag; -import com.flowpowered.nbt.EndTag; -import com.flowpowered.nbt.FloatTag; -import com.flowpowered.nbt.IntArrayTag; -import com.flowpowered.nbt.IntTag; -import com.flowpowered.nbt.ListTag; -import com.flowpowered.nbt.LongTag; -import com.flowpowered.nbt.NBTConstants; -import com.flowpowered.nbt.ShortArrayTag; -import com.flowpowered.nbt.ShortTag; -import com.flowpowered.nbt.StringTag; -import com.flowpowered.nbt.Tag; -import com.flowpowered.nbt.TagType; +import com.flowpowered.nbt.*; /** - * This class reads NBT, or Named Binary Tag streams, and produces an object graph of subclasses of the {@link Tag} object.

The NBT format was created by Markus Persson, and the specification - * may be found at https://flowpowered.com/nbt/spec.txt. + * This class reads NBT, or Named Binary Tag streams, and produces an object graph of subclasses of the {@link Tag} object. + *

+ * The NBT format was created by Markus Persson, and the specification may be found at + * https://flowpowered.com/nbt/spec.txt. */ public final class NBTInputStream implements Closeable { - /** - * The data input stream. - */ - private final EndianSwitchableInputStream is; - - /** - * Creates a new {@link NBTInputStream}, which will source its data from the specified input stream. This assumes the stream is compressed. - * - * @param is The input stream. - * @throws java.io.IOException if an I/O error occurs. - */ - public NBTInputStream(InputStream is) throws IOException { - this(is, true, ByteOrder.BIG_ENDIAN); - } - - /** - * Creates a new {@link NBTInputStream}, which sources its data from the specified input stream. A flag must be passed which indicates if the stream is compressed with GZIP or not. This assumes the - * stream uses big endian encoding. - * - * @param is The input stream. - * @param compressed A flag indicating if the stream is compressed. - * @throws java.io.IOException if an I/O error occurs. - */ - public NBTInputStream(InputStream is, boolean compressed) throws IOException { - this(is, compressed, ByteOrder.BIG_ENDIAN); - } - - /** - * Creates a new {@link NBTInputStream}, which sources its data from the specified input stream. A flag must be passed which indicates if the stream is compressed with GZIP or not. - * - * @param is The input stream. - * @param compressed A flag indicating if the stream is compressed. - * @param endianness Whether to read numbers from the InputStream with little endian encoding. - * @throws java.io.IOException if an I/O error occurs. - */ - public NBTInputStream(InputStream is, boolean compressed, ByteOrder endianness) throws IOException { - this.is = new EndianSwitchableInputStream(compressed ? new GZIPInputStream(is) : is, endianness); - } - - /** - * Reads an NBT {@link Tag} from the stream. - * - * @return The tag that was read. - * @throws java.io.IOException if an I/O error occurs. - */ - public Tag readTag() throws IOException { - return readTag(0); - } - - /** - * Reads an NBT {@link Tag} from the stream. - * - * @param depth The depth of this tag. - * @return The tag that was read. - * @throws java.io.IOException if an I/O error occurs. - */ - private Tag readTag(int depth) throws IOException { - int typeId = is.readByte() & 0xFF; - TagType type = TagType.getById(typeId); - - String name; - if (type != TagType.TAG_END) { - int nameLength = is.readShort() & 0xFFFF; - byte[] nameBytes = new byte[nameLength]; - is.readFully(nameBytes); - name = new String(nameBytes, NBTConstants.CHARSET.name()); - } else { - name = ""; - } - - return readTagPayload(type, name, depth); - } - - /** - * Reads the payload of a {@link Tag}, given the name and type. - * - * @param type The type. - * @param name The name. - * @param depth The depth. - * @return The tag. - * @throws java.io.IOException if an I/O error occurs. - */ - @SuppressWarnings ({"unchecked", "rawtypes"}) - private Tag readTagPayload(TagType type, String name, int depth) throws IOException { - switch (type) { - case TAG_END: - if (depth == 0) { - throw new IOException("TAG_End found without a TAG_Compound/TAG_List tag preceding it."); - } else { - return new EndTag(); - } - - case TAG_BYTE: - return new ByteTag(name, is.readByte()); - - case TAG_SHORT: - return new ShortTag(name, is.readShort()); - - case TAG_INT: - return new IntTag(name, is.readInt()); - - case TAG_LONG: - return new LongTag(name, is.readLong()); - - case TAG_FLOAT: - return new FloatTag(name, is.readFloat()); - - case TAG_DOUBLE: - return new DoubleTag(name, is.readDouble()); - - case TAG_BYTE_ARRAY: - int length = is.readInt(); - byte[] bytes = new byte[length]; - is.readFully(bytes); - return new ByteArrayTag(name, bytes); - - case TAG_STRING: - length = is.readShort(); - bytes = new byte[length]; - is.readFully(bytes); - return new StringTag(name, new String(bytes, NBTConstants.CHARSET.name())); - - case TAG_LIST: - TagType childType = TagType.getById(is.readByte()); - length = is.readInt(); - - Class clazz = childType.getTagClass(); - List tagList = new ArrayList(length); - for (int i = 0; i < length; i++) { - Tag tag = readTagPayload(childType, "", depth + 1); - if (tag instanceof EndTag) { - throw new IOException("TAG_End not permitted in a list."); - } else if (!clazz.isInstance(tag)) { - throw new IOException("Mixed tag types within a list."); - } - tagList.add(tag); - } - - return new ListTag(name, clazz, tagList); - - case TAG_COMPOUND: - CompoundMap compoundTagList = new CompoundMap(); - while (true) { - Tag tag = readTag(depth + 1); - if (tag instanceof EndTag) { - break; - } else { - compoundTagList.put(tag); - } - } - - return new CompoundTag(name, compoundTagList); - - case TAG_INT_ARRAY: - length = is.readInt(); - int[] ints = new int[length]; - for (int i = 0; i < length; i++) { - ints[i] = is.readInt(); - } - return new IntArrayTag(name, ints); - - case TAG_SHORT_ARRAY: - length = is.readInt(); - short[] shorts = new short[length]; - for (int i = 0; i < length; i++) { - shorts[i] = is.readShort(); - } - return new ShortArrayTag(name, shorts); - - default: - throw new IOException("Invalid tag type: " + type + "."); - } - } - - public void close() throws IOException { - is.close(); - } - - /** - * @return whether this NBTInputStream reads numbers in little-endian format. - */ - public ByteOrder getByteOrder() { - return is.getEndianness(); - } + + /** + * Flag indicating that the given data stream is not compressed. + */ + public static final int NO_COMPRESSION = 0; + /** + * Flag indicating that the given data will be compressed with the GZIP compression algorithm. This is the default compression method used + * to compress nbt files. Chunks in Minecraft Region/Anvil files with compression method {@code 1} (see the respective format documentation) + * will use this compression method too, although this is not actively used anymore. + */ + public static final int GZIP_COMPRESSION = 1; + /** + * Flag indicating that the given data will be compressed with the ZLIB compression algorithm. This is the default compression method used + * to compress the nbt data of the chunks in Minecraft Region/Anvil files, but only if its compression method is {@code 2} (see the + * respective format documentation), which is default for all newer versions. + */ + public static final int ZLIB_COMPRESSION = 2; + + private final DataInput dataIn; + private final InputStream inputStream; + + /** + * Creates a new {@link NBTInputStream}, which will source its data from the specified input stream. This assumes the stream is compressed. + * + * @param is + * The input stream. + * @throws java.io.IOException + * if an I/O error occurs. + */ + public NBTInputStream(InputStream is) throws IOException { + this(is, GZIP_COMPRESSION, ByteOrder.BIG_ENDIAN); + } + + /** + * Creates a new {@link NBTInputStream}, which sources its data from the specified input stream. A flag must be passed which indicates if + * the stream is compressed with GZIP or not. This assumes the stream uses big endian encoding. + * + * @param is + * The input stream. + * @param compressed + * A flag indicating if the stream is compressed. + * @throws java.io.IOException + * if an I/O error occurs. + * @deprecated Use {@link #NBTInputStream(InputStream, int)} instead + */ + @Deprecated + public NBTInputStream(InputStream is, boolean compressed) throws IOException { + this(is, compressed, ByteOrder.BIG_ENDIAN); + } + + /** + * Creates a new {@link NBTInputStream}, which will source its data from the specified input stream. The stream may be wrapped into a + * decompressing input stream depending on the chosen compression method. This assumes the stream uses big endian encoding. + * + * @param is + * The input stream. + * @param compression + * The compression algorithm used for the input stream. Must be {@link #NO_COMPRESSION}, {@link #GZIP_COMPRESSION} or + * {@link #ZLIB_COMPRESSION}. + * @throws java.io.IOException + * if an I/O error occurs. + */ + public NBTInputStream(InputStream is, int compression) throws IOException { + this(is, compression, ByteOrder.BIG_ENDIAN); + } + + /** + * Creates a new {@link NBTInputStream}, which sources its data from the specified input stream. A flag must be passed which indicates if + * the stream is compressed with GZIP or not. + * + * @param is + * The input stream. + * @param compressed + * A flag indicating if the stream is compressed. + * @param endianness + * Whether to read numbers from the InputStream with little endian encoding. + * @throws java.io.IOException + * if an I/O error occurs. + * @deprecated Use {@link #NBTInputStream(InputStream, int, ByteOrder)} instead + */ + @Deprecated + public NBTInputStream(InputStream is, boolean compressed, ByteOrder endianness) throws IOException { + this(is, compressed ? GZIP_COMPRESSION : NO_COMPRESSION, endianness); + } + + /** + * Creates a new {@link NBTInputStream}, which sources its data from the specified input stream. The stream may be wrapped into a + * decompressing input stream depending on the chosen compression method. + * + * @param is + * The input stream. + * @param compression + * The compression algorithm used for the input stream. Must be {@link #NO_COMPRESSION}, {@link #GZIP_COMPRESSION} or + * {@link #ZLIB_COMPRESSION}. + * @param endianness + * Whether to read numbers from the InputStream with little endian encoding. + * @throws java.io.IOException + * if an I/O error occurs. + */ + public NBTInputStream(InputStream is, int compression, ByteOrder endianness) throws IOException { + switch (compression) { + case NO_COMPRESSION: + break; + case GZIP_COMPRESSION: + is = new GZIPInputStream(is); + break; + case ZLIB_COMPRESSION: + is = new InflaterInputStream(is); + break; + default: + throw new IllegalArgumentException("Unsupported compression type, must be between 0 and 2 (inclusive)"); + } + if (endianness == ByteOrder.LITTLE_ENDIAN) + this.inputStream = (InputStream) (this.dataIn = new LittleEndianInputStream(is)); + else + this.inputStream = (InputStream) (this.dataIn = new DataInputStream(is)); + } + + /** + * Reads an NBT {@link Tag} from the stream. + * + * @return The tag that was read. + * @throws java.io.IOException + * if an I/O error occurs. + */ + public Tag readTag() throws IOException { + return readTag(0); + } + + /** + * Reads an NBT {@link Tag} from the stream. + * + * @param depth + * The depth of this tag. + * @return The tag that was read. + * @throws java.io.IOException + * if an I/O error occurs. + */ + private Tag readTag(int depth) throws IOException { + int typeId = dataIn.readByte() & 0xFF; + TagType type = TagType.getById(typeId); + + String name; + if (type != TagType.TAG_END) { + int nameLength = dataIn.readShort() & 0xFFFF; + byte[] nameBytes = new byte[nameLength]; + dataIn.readFully(nameBytes); + name = new String(nameBytes, NBTConstants.CHARSET.name()); + } else { + name = ""; + } + + return readTagPayload(type, name, depth); + } + + /** + * Reads the payload of a {@link Tag}, given the name and type. + * + * @param type + * The type. + * @param name + * The name. + * @param depth + * The depth. + * @return The tag. + * @throws java.io.IOException + * if an I/O error occurs. + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + private Tag readTagPayload(TagType type, String name, int depth) throws IOException { + switch (type) { + case TAG_END: + if (depth == 0) { + throw new IOException("TAG_End found without a TAG_Compound/TAG_List tag preceding it."); + } else { + return new EndTag(); + } + + case TAG_BYTE: + return new ByteTag(name, dataIn.readByte()); + + case TAG_SHORT: + return new ShortTag(name, dataIn.readShort()); + + case TAG_INT: + return new IntTag(name, dataIn.readInt()); + + case TAG_LONG: + return new LongTag(name, dataIn.readLong()); + + case TAG_FLOAT: + return new FloatTag(name, dataIn.readFloat()); + + case TAG_DOUBLE: + return new DoubleTag(name, dataIn.readDouble()); + + case TAG_BYTE_ARRAY: + int length = dataIn.readInt(); + byte[] bytes = new byte[length]; + dataIn.readFully(bytes); + return new ByteArrayTag(name, bytes); + + case TAG_STRING: + length = dataIn.readShort(); + bytes = new byte[length]; + dataIn.readFully(bytes); + return new StringTag(name, new String(bytes, NBTConstants.CHARSET.name())); + + case TAG_LIST: + TagType childType = TagType.getById(dataIn.readByte()); + length = dataIn.readInt(); + + Class clazz = childType.getTagClass(); + List tagList = new ArrayList(length); + for (int i = 0; i < length; i++) { + Tag tag = readTagPayload(childType, "", depth + 1); + if (tag instanceof EndTag) { + throw new IOException("TAG_End not permitted in a list."); + } else if (!clazz.isInstance(tag)) { + throw new IOException("Mixed tag types within a list."); + } + tagList.add(tag); + } + + return new ListTag(name, clazz, tagList); + + case TAG_COMPOUND: + CompoundMap compoundTagList = new CompoundMap(); + while (true) { + Tag tag = readTag(depth + 1); + if (tag instanceof EndTag) { + break; + } else { + compoundTagList.put(tag); + } + } + + return new CompoundTag(name, compoundTagList); + + case TAG_INT_ARRAY: + length = dataIn.readInt(); + int[] ints = new int[length]; + for (int i = 0; i < length; i++) { + ints[i] = dataIn.readInt(); + } + return new IntArrayTag(name, ints); + + case TAG_LONG_ARRAY: + length = dataIn.readInt(); + long[] longs = new long[length]; + for (int i = 0; i < length; i++) { + longs[i] = dataIn.readLong(); + } + return new LongArrayTag(name, longs); + + case TAG_SHORT_ARRAY: + length = dataIn.readInt(); + short[] shorts = new short[length]; + for (int i = 0; i < length; i++) { + shorts[i] = dataIn.readShort(); + } + return new ShortArrayTag(name, shorts); + + default: + throw new IOException("Invalid tag type: " + type + "."); + } + } + + @Override + public void close() throws IOException { + inputStream.close(); + } + + /** + * @return whether this NBTInputStream reads numbers in little-endian format. + */ + @Deprecated + public ByteOrder getByteOrder() { + throw new UnsupportedOperationException(); + } } diff --git a/src/main/java/com/flowpowered/nbt/stream/NBTOutputStream.java b/src/main/java/com/flowpowered/nbt/stream/NBTOutputStream.java index bdb9504..e9756ca 100644 --- a/src/main/java/com/flowpowered/nbt/stream/NBTOutputStream.java +++ b/src/main/java/com/flowpowered/nbt/stream/NBTOutputStream.java @@ -24,326 +24,432 @@ package com.flowpowered.nbt.stream; import java.io.Closeable; +import java.io.DataOutput; +import java.io.DataOutputStream; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteOrder; import java.util.List; +import java.util.zip.DeflaterOutputStream; import java.util.zip.GZIPOutputStream; -import com.flowpowered.nbt.ByteArrayTag; -import com.flowpowered.nbt.ByteTag; -import com.flowpowered.nbt.CompoundTag; -import com.flowpowered.nbt.DoubleTag; -import com.flowpowered.nbt.EndTag; -import com.flowpowered.nbt.FloatTag; -import com.flowpowered.nbt.IntArrayTag; -import com.flowpowered.nbt.IntTag; -import com.flowpowered.nbt.ListTag; -import com.flowpowered.nbt.LongTag; -import com.flowpowered.nbt.NBTConstants; -import com.flowpowered.nbt.ShortArrayTag; -import com.flowpowered.nbt.ShortTag; -import com.flowpowered.nbt.StringTag; -import com.flowpowered.nbt.Tag; -import com.flowpowered.nbt.TagType; +import com.flowpowered.nbt.*; /** - * This class writes NBT, or Named Binary Tag, {@link Tag} objects to an underlying {@link java.io.OutputStream}.

The NBT format was created by Markus Persson, and the specification may be found - * at https://flowpowered.com/nbt/spec.txt. + * This class writes NBT, or Named Binary Tag, {@link Tag} objects to an underlying {@link java.io.OutputStream}. + *

+ * The NBT format was created by Markus Persson, and the specification may be found at + * https://flowpowered.com/nbt/spec.txt. */ public final class NBTOutputStream implements Closeable { - /** - * The output stream. - */ - private final EndianSwitchableOutputStream os; - - /** - * Creates a new {@link NBTOutputStream}, which will write data to the specified underlying output stream. This assumes the output stream should be compressed with GZIP. - * - * @param os The output stream. - * @throws java.io.IOException if an I/O error occurs. - */ - public NBTOutputStream(OutputStream os) throws IOException { - this(os, true, ByteOrder.BIG_ENDIAN); - } - - /** - * Creates a new {@link NBTOutputStream}, which will write data to the specified underlying output stream. A flag indicates if the output should be compressed with GZIP or not. - * - * @param os The output stream. - * @param compressed A flag that indicates if the output should be compressed. - * @throws java.io.IOException if an I/O error occurs. - */ - public NBTOutputStream(OutputStream os, boolean compressed) throws IOException { - this(os, compressed, ByteOrder.BIG_ENDIAN); - } - - /** - * Creates a new {@link NBTOutputStream}, which will write data to the specified underlying output stream. A flag indicates if the output should be compressed with GZIP or not. - * - * @param os The output stream. - * @param compressed A flag that indicates if the output should be compressed. - * @param endianness A flag that indicates if numbers in the output should be output in little-endian format. - * @throws java.io.IOException if an I/O error occurs. - */ - public NBTOutputStream(OutputStream os, boolean compressed, ByteOrder endianness) throws IOException { - this.os = new EndianSwitchableOutputStream(compressed ? new GZIPOutputStream(os) : os, endianness); - } - - /** - * Writes a tag. - * - * @param tag The tag to write. - * @throws java.io.IOException if an I/O error occurs. - */ - public void writeTag(Tag tag) throws IOException { - String name = tag.getName(); - byte[] nameBytes = name.getBytes(NBTConstants.CHARSET.name()); - - os.writeByte(tag.getType().getId()); - os.writeShort(nameBytes.length); - os.write(nameBytes); - - if (tag.getType() == TagType.TAG_END) { - throw new IOException("Named TAG_End not permitted."); - } - - writeTagPayload(tag); - } - - /** - * Writes tag payload. - * - * @param tag The tag. - * @throws java.io.IOException if an I/O error occurs. - */ - private void writeTagPayload(Tag tag) throws IOException { - switch (tag.getType()) { - case TAG_END: - writeEndTagPayload((EndTag) tag); - break; - - case TAG_BYTE: - writeByteTagPayload((ByteTag) tag); - break; - - case TAG_SHORT: - writeShortTagPayload((ShortTag) tag); - break; - - case TAG_INT: - writeIntTagPayload((IntTag) tag); - break; - - case TAG_LONG: - writeLongTagPayload((LongTag) tag); - break; - - case TAG_FLOAT: - writeFloatTagPayload((FloatTag) tag); - break; - - case TAG_DOUBLE: - writeDoubleTagPayload((DoubleTag) tag); - break; - - case TAG_BYTE_ARRAY: - writeByteArrayTagPayload((ByteArrayTag) tag); - break; - - case TAG_STRING: - writeStringTagPayload((StringTag) tag); - break; - - case TAG_LIST: - writeListTagPayload((ListTag) tag); - break; - - case TAG_COMPOUND: - writeCompoundTagPayload((CompoundTag) tag); - break; - - case TAG_INT_ARRAY: - writeIntArrayTagPayload((IntArrayTag) tag); - break; - - case TAG_SHORT_ARRAY: - writeShortArrayTagPayload((ShortArrayTag) tag); - break; - - default: - throw new IOException("Invalid tag type: " + tag.getType() + "."); - } - } - - /** - * Writes a {@code TAG_Byte} tag. - * - * @param tag The tag. - * @throws java.io.IOException if an I/O error occurs. - */ - private void writeByteTagPayload(ByteTag tag) throws IOException { - os.writeByte(tag.getValue()); - } - - /** - * Writes a {@code TAG_Byte_Array} tag. - * - * @param tag The tag. - * @throws java.io.IOException if an I/O error occurs. - */ - private void writeByteArrayTagPayload(ByteArrayTag tag) throws IOException { - byte[] bytes = tag.getValue(); - os.writeInt(bytes.length); - os.write(bytes); - } - - /** - * Writes a {@code TAG_Compound} tag. - * - * @param tag The tag. - * @throws java.io.IOException if an I/O error occurs. - */ - private void writeCompoundTagPayload(CompoundTag tag) throws IOException { - for (Tag childTag : tag.getValue().values()) { - writeTag(childTag); - } - os.writeByte(TagType.TAG_END.getId()); // end tag - better way? - } - - /** - * Writes a {@code TAG_List} tag. - * - * @param tag The tag. - * @throws java.io.IOException if an I/O error occurs. - */ - @SuppressWarnings ("unchecked") - private void writeListTagPayload(ListTag tag) throws IOException { - Class> clazz = tag.getElementType(); - List> tags = (List>) tag.getValue(); - int size = tags.size(); - - os.writeByte(TagType.getByTagClass(clazz).getId()); - os.writeInt(size); - for (Tag tag1 : tags) { - writeTagPayload(tag1); - } - } - - /** - * Writes a {@code TAG_String} tag. - * - * @param tag The tag. - * @throws java.io.IOException if an I/O error occurs. - */ - private void writeStringTagPayload(StringTag tag) throws IOException { - byte[] bytes = tag.getValue().getBytes(NBTConstants.CHARSET.name()); - os.writeShort(bytes.length); - os.write(bytes); - } - - /** - * Writes a {@code TAG_Double} tag. - * - * @param tag The tag. - * @throws java.io.IOException if an I/O error occurs. - */ - private void writeDoubleTagPayload(DoubleTag tag) throws IOException { - os.writeDouble(tag.getValue()); - } - - /** - * Writes a {@code TAG_Float} tag. - * - * @param tag The tag. - * @throws java.io.IOException if an I/O error occurs. - */ - private void writeFloatTagPayload(FloatTag tag) throws IOException { - os.writeFloat(tag.getValue()); - } - - /** - * Writes a {@code TAG_Long} tag. - * - * @param tag The tag. - * @throws java.io.IOException if an I/O error occurs. - */ - private void writeLongTagPayload(LongTag tag) throws IOException { - os.writeLong(tag.getValue()); - } - - /** - * Writes a {@code TAG_Int} tag. - * - * @param tag The tag. - * @throws java.io.IOException if an I/O error occurs. - */ - private void writeIntTagPayload(IntTag tag) throws IOException { - os.writeInt(tag.getValue()); - } - - /** - * Writes a {@code TAG_Short} tag. - * - * @param tag The tag. - * @throws java.io.IOException if an I/O error occurs. - */ - private void writeShortTagPayload(ShortTag tag) throws IOException { - os.writeShort(tag.getValue()); - } - - /** - * Writes a {@code TAG_Int_Array} tag. - * - * @param tag The tag. - * @throws java.io.IOException if an I/O error occurs. - */ - private void writeIntArrayTagPayload(IntArrayTag tag) throws IOException { - int[] ints = tag.getValue(); - os.writeInt(ints.length); - for (int i = 0; i < ints.length; i++) { - os.writeInt(ints[i]); - } - } - - /** - * Writes a {@code TAG_Short_Array} tag. - * - * @param tag The tag. - * @throws java.io.IOException if an I/O error occurs. - */ - private void writeShortArrayTagPayload(ShortArrayTag tag) throws IOException { - short[] shorts = tag.getValue(); - os.writeInt(shorts.length); - for (int i = 0; i < shorts.length; i++) { - os.writeShort(shorts[i]); - } - } - - /** - * Writes a {@code TAG_Empty} tag. - * - * @param tag The tag. - */ - private void writeEndTagPayload(EndTag tag) { - /* empty */ - } - - public void close() throws IOException { - os.close(); - } - - /** - * @return whether this NBTInputStream writes numbers in little-endian format. - */ - public ByteOrder getEndianness() { - return os.getEndianness(); - } - - /** - * Flushes the stream - */ - public void flush() throws IOException { - os.flush(); - } + + private final DataOutput dataOut; + private final OutputStream outputStream; + + /** + * Creates a new {@link NBTOutputStream}, which will write data to the specified underlying output stream. This assumes the output stream + * should be compressed with GZIP. + * + * @param os + * The output stream. + * @throws java.io.IOException + * if an I/O error occurs. + */ + public NBTOutputStream(OutputStream os) throws IOException { + this(os, NBTInputStream.GZIP_COMPRESSION, ByteOrder.BIG_ENDIAN); + } + + /** + * Creates a new {@link NBTOutputStream}, which will write data to the specified underlying output stream. A flag indicates if the output + * should be compressed with GZIP or not. + * + * @param os + * The output stream. + * @param compressed + * A flag that indicates if the output should be compressed. + * @throws java.io.IOException + * if an I/O error occurs. + * @deprecated Use {@link #NBTOutputStream(InputStream, int)} instead + */ + @Deprecated + public NBTOutputStream(OutputStream os, boolean compressed) throws IOException { + this(os, compressed, ByteOrder.BIG_ENDIAN); + } + + /** + * Creates a new {@link NBTOutputStream}, which will write data to the specified underlying output stream. The stream may be wrapped into a + * compressing output stream depending on the chosen compression method. A flag indicates if the output should be compressed with GZIP or + * not. + * + * @param os + * The output stream. + * @param compression + * The compression algorithm used for the input stream. Must be {@link NBTInputStream#NO_COMPRESSION}, + * {@link NBTInputStream#GZIP_COMPRESSION} or {@link NBTInputStream#ZLIB_COMPRESSION}. + * @throws java.io.IOException + * if an I/O error occurs. + */ + public NBTOutputStream(OutputStream os, int compression) throws IOException { + this(os, compression, ByteOrder.BIG_ENDIAN); + } + + /** + * Creates a new {@link NBTOutputStream}, which will write data to the specified underlying output stream. A flag indicates if the output + * should be compressed with GZIP or not. + * + * @param os + * The output stream. + * @param compressed + * A flag that indicates if the output should be compressed. + * @param endianness + * A flag that indicates if numbers in the output should be output in little-endian format. + * @throws java.io.IOException + * if an I/O error occurs. + * @deprecated Use {@link #NBTOutputStream(InputStream, int, ByteOrder)} instead + */ + @Deprecated + public NBTOutputStream(OutputStream os, boolean compressed, ByteOrder endianness) throws IOException { + this(os, compressed ? NBTInputStream.GZIP_COMPRESSION : NBTInputStream.NO_COMPRESSION, endianness); + } + + /** + * Creates a new {@link NBTOutputStream}, which will write data to the specified underlying output stream. The stream may be wrapped into a + * compressing output stream depending on the chosen compression method. + * + * @param os + * The output stream. + * @param compression + * The compression algorithm used for the input stream. Must be {@link NBTInputStream#NO_COMPRESSION}, + * {@link NBTInputStream#GZIP_COMPRESSION} or {@link NBTInputStream#ZLIB_COMPRESSION}. + * @param endianness + * A flag that indicates if numbers in the output should be output in little-endian format. + * @throws java.io.IOException + * if an I/O error occurs. + */ + public NBTOutputStream(OutputStream os, int compression, ByteOrder endianness) throws IOException { + switch (compression) { + case NBTInputStream.NO_COMPRESSION: + break; + case NBTInputStream.GZIP_COMPRESSION: + os = new GZIPOutputStream(os); + break; + case NBTInputStream.ZLIB_COMPRESSION: + os = new DeflaterOutputStream(os); + break; + default: + throw new IllegalArgumentException("Unsupported compression type, must be between 0 and 2 (inclusive)"); + } + if (endianness == ByteOrder.LITTLE_ENDIAN) + this.outputStream = (OutputStream) (this.dataOut = new LittleEndianOutputStream(os)); + else + this.outputStream = (OutputStream) (this.dataOut = new DataOutputStream(os)); + } + + /** + * Writes a tag. + * + * @param tag + * The tag to write. + * @throws java.io.IOException + * if an I/O error occurs. + */ + public void writeTag(Tag tag) throws IOException { + String name = tag.getName(); + byte[] nameBytes = name.getBytes(NBTConstants.CHARSET.name()); + + dataOut.writeByte(tag.getType().getId()); + dataOut.writeShort(nameBytes.length); + dataOut.write(nameBytes); + + if (tag.getType() == TagType.TAG_END) { + throw new IOException("Named TAG_End not permitted."); + } + + writeTagPayload(tag); + } + + /** + * Writes tag payload. + * + * @param tag + * The tag. + * @throws java.io.IOException + * if an I/O error occurs. + */ + private void writeTagPayload(Tag tag) throws IOException { + switch (tag.getType()) { + case TAG_END: + writeEndTagPayload((EndTag) tag); + break; + + case TAG_BYTE: + writeByteTagPayload((ByteTag) tag); + break; + + case TAG_SHORT: + writeShortTagPayload((ShortTag) tag); + break; + + case TAG_INT: + writeIntTagPayload((IntTag) tag); + break; + + case TAG_LONG: + writeLongTagPayload((LongTag) tag); + break; + + case TAG_FLOAT: + writeFloatTagPayload((FloatTag) tag); + break; + + case TAG_DOUBLE: + writeDoubleTagPayload((DoubleTag) tag); + break; + + case TAG_BYTE_ARRAY: + writeByteArrayTagPayload((ByteArrayTag) tag); + break; + + case TAG_STRING: + writeStringTagPayload((StringTag) tag); + break; + + case TAG_LIST: + writeListTagPayload((ListTag) tag); + break; + + case TAG_COMPOUND: + writeCompoundTagPayload((CompoundTag) tag); + break; + + case TAG_INT_ARRAY: + writeIntArrayTagPayload((IntArrayTag) tag); + break; + + case TAG_LONG_ARRAY: + writeLongArrayTagPayload((LongArrayTag) tag); + break; + + case TAG_SHORT_ARRAY: + writeShortArrayTagPayload((ShortArrayTag) tag); + break; + + default: + throw new IOException("Invalid tag type: " + tag.getType() + "."); + } + } + + /** + * Writes a {@code TAG_Byte} tag. + * + * @param tag + * The tag. + * @throws java.io.IOException + * if an I/O error occurs. + */ + private void writeByteTagPayload(ByteTag tag) throws IOException { + dataOut.writeByte(tag.getValue()); + } + + /** + * Writes a {@code TAG_Byte_Array} tag. + * + * @param tag + * The tag. + * @throws java.io.IOException + * if an I/O error occurs. + */ + private void writeByteArrayTagPayload(ByteArrayTag tag) throws IOException { + byte[] bytes = tag.getValue(); + dataOut.writeInt(bytes.length); + dataOut.write(bytes); + } + + /** + * Writes a {@code TAG_Compound} tag. + * + * @param tag + * The tag. + * @throws java.io.IOException + * if an I/O error occurs. + */ + private void writeCompoundTagPayload(CompoundTag tag) throws IOException { + for (Tag childTag : tag.getValue().values()) { + writeTag(childTag); + } + dataOut.writeByte(TagType.TAG_END.getId()); // end tag - better way? + } + + /** + * Writes a {@code TAG_List} tag. + * + * @param tag + * The tag. + * @throws java.io.IOException + * if an I/O error occurs. + */ + @SuppressWarnings("unchecked") + private void writeListTagPayload(ListTag tag) throws IOException { + Class> clazz = tag.getElementType(); + List> tags = (List>) tag.getValue(); + int size = tags.size(); + + dataOut.writeByte(TagType.getByTagClass(clazz).getId()); + dataOut.writeInt(size); + for (Tag tag1 : tags) { + writeTagPayload(tag1); + } + } + + /** + * Writes a {@code TAG_String} tag. + * + * @param tag + * The tag. + * @throws java.io.IOException + * if an I/O error occurs. + */ + private void writeStringTagPayload(StringTag tag) throws IOException { + byte[] bytes = tag.getValue().getBytes(NBTConstants.CHARSET.name()); + dataOut.writeShort(bytes.length); + dataOut.write(bytes); + } + + /** + * Writes a {@code TAG_Double} tag. + * + * @param tag + * The tag. + * @throws java.io.IOException + * if an I/O error occurs. + */ + private void writeDoubleTagPayload(DoubleTag tag) throws IOException { + dataOut.writeDouble(tag.getValue()); + } + + /** + * Writes a {@code TAG_Float} tag. + * + * @param tag + * The tag. + * @throws java.io.IOException + * if an I/O error occurs. + */ + private void writeFloatTagPayload(FloatTag tag) throws IOException { + dataOut.writeFloat(tag.getValue()); + } + + /** + * Writes a {@code TAG_Long} tag. + * + * @param tag + * The tag. + * @throws java.io.IOException + * if an I/O error occurs. + */ + private void writeLongTagPayload(LongTag tag) throws IOException { + dataOut.writeLong(tag.getValue()); + } + + /** + * Writes a {@code TAG_Int} tag. + * + * @param tag + * The tag. + * @throws java.io.IOException + * if an I/O error occurs. + */ + private void writeIntTagPayload(IntTag tag) throws IOException { + dataOut.writeInt(tag.getValue()); + } + + /** + * Writes a {@code TAG_Short} tag. + * + * @param tag + * The tag. + * @throws java.io.IOException + * if an I/O error occurs. + */ + private void writeShortTagPayload(ShortTag tag) throws IOException { + dataOut.writeShort(tag.getValue()); + } + + /** + * Writes a {@code TAG_Int_Array} tag. + * + * @param tag + * The tag. + * @throws java.io.IOException + * if an I/O error occurs. + */ + private void writeIntArrayTagPayload(IntArrayTag tag) throws IOException { + int[] ints = tag.getValue(); + dataOut.writeInt(ints.length); + for (int i = 0; i < ints.length; i++) { + dataOut.writeInt(ints[i]); + } + } + + /** + * Writes a {@code TAG_Long_Array} tag. + * + * @param tag + * The tag. + * @throws java.io.IOException + * if an I/O error occurs. + */ + private void writeLongArrayTagPayload(LongArrayTag tag) throws IOException { + long[] longs = tag.getValue(); + dataOut.writeInt(longs.length); + for (int i = 0; i < longs.length; i++) { + dataOut.writeLong(longs[i]); + } + } + + /** + * Writes a {@code TAG_Short_Array} tag. + * + * @param tag + * The tag. + * @throws java.io.IOException + * if an I/O error occurs. + */ + private void writeShortArrayTagPayload(ShortArrayTag tag) throws IOException { + short[] shorts = tag.getValue(); + dataOut.writeInt(shorts.length); + for (int i = 0; i < shorts.length; i++) { + dataOut.writeShort(shorts[i]); + } + } + + /** + * Writes a {@code TAG_Empty} tag. + * + * @param tag + * The tag. + */ + private void writeEndTagPayload(EndTag tag) { + /* empty */ + } + + @Override + public void close() throws IOException { + outputStream.close(); + } + + /** + * @return whether this NBTInputStream writes numbers in little-endian format. + */ + @Deprecated + public ByteOrder getEndianness() { + throw new UnsupportedOperationException(); + } + + /** + * Flushes the stream + */ + public void flush() throws IOException { + outputStream.flush(); + } } diff --git a/src/test/java/com/flowpowered/nbt/LongTest.java b/src/test/java/com/flowpowered/nbt/LongTest.java new file mode 100644 index 0000000..d1f7762 --- /dev/null +++ b/src/test/java/com/flowpowered/nbt/LongTest.java @@ -0,0 +1,53 @@ +package com.flowpowered.nbt; + +import static org.junit.Assert.assertEquals; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.LongBuffer; +import java.util.Random; + +import org.junit.Test; + +import com.flowpowered.nbt.regionfile.Chunk; + +public class LongTest { + + @Test + public void test() { + for (int BITS = 4; BITS <= 12; BITS++) { + Random random = new Random(1234); + + // Fill a byte buffer with random data + ByteBuffer buffer = ByteBuffer.wrap(new byte[BITS * 64 * 8]); + buffer.order(ByteOrder.BIG_ENDIAN); + random.nextBytes(buffer.array()); + + // Convert it to a long buffer + LongBuffer data = buffer.asLongBuffer(); + data.rewind(); + long[] longData = new long[BITS * 64]; + data.get(longData); + + // Control data: Convert all bytes to a binary string and zero-pad them, append them to a large bit string + // Slicing the bit string into equal length substrings will give the control data. + // Add a double reverse because Mojang does silly stuff sometimes. + StringBuffer number = new StringBuffer(); + for (long l : longData) + number.append(convertLong(Long.reverse(l))); + for (int i = 0; i < BITS * 64; i++) { + // Compare conversion with control data + assertEquals(Long.parseLong(new StringBuilder(number.substring(i * BITS, i * BITS + BITS)).reverse().toString(), 2), + Chunk.extractFromLong(longData, i, BITS)); + } + } + } + + /** Convert long to binary string and zero pad it */ + private static String convertLong(long l) { + String s = Long.toBinaryString(l); + // Fancy way of zero padding :) + s = "0000000000000000000000000000000000000000000000000000000000000000".substring(s.length()) + s; + return s; + } +} diff --git a/src/test/java/com/flowpowered/nbt/RegionFileTest.java b/src/test/java/com/flowpowered/nbt/RegionFileTest.java new file mode 100644 index 0000000..751c756 --- /dev/null +++ b/src/test/java/com/flowpowered/nbt/RegionFileTest.java @@ -0,0 +1,52 @@ +package com.flowpowered.nbt; + +import static org.junit.Assert.assertEquals; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import com.flowpowered.nbt.regionfile.Chunk; +import com.flowpowered.nbt.regionfile.RegionFile; + +public class RegionFileTest { + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + /** + * Test reading the NBT data in a region file + * + * @throws URISyntaxException + * @throws IOException + */ + @Test + public void testRead() throws IOException, URISyntaxException { + try (RegionFile file = new RegionFile(Paths.get(getClass().getResource("/r.1.3.mca").toURI()));) { + for (int i : file.listChunks()) { + Chunk chunk = file.loadChunk(i); + if (chunk != null) + chunk.readTag(); + } + } + } + + @Test + public void testCreateNew() throws IOException { + File file = folder.newFile(); + file.delete(); + RegionFile.createNew(file.toPath()).close(); + file.delete(); + RegionFile rf = RegionFile.open(folder.newFolder().toPath().resolve("test").resolve("test.mca")); + rf.writeChunks(new HashMap<>()); + assertEquals(4096 * 2, Files.size(rf.getPath())); + rf.close(); + } +} diff --git a/src/test/java/com/flowpowered/nbt/stream/EndianSwitchableStreamTest.java b/src/test/java/com/flowpowered/nbt/stream/EndianSwitchableStreamTest.java index a2a7e12..380955b 100644 --- a/src/test/java/com/flowpowered/nbt/stream/EndianSwitchableStreamTest.java +++ b/src/test/java/com/flowpowered/nbt/stream/EndianSwitchableStreamTest.java @@ -23,17 +23,16 @@ */ package com.flowpowered.nbt.stream; +import static org.junit.Assert.assertEquals; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.nio.ByteOrder; import org.junit.Test; -import static org.junit.Assert.assertEquals; - /** - * Test for both {@link EndianSwitchableInputStream EndianSwitchableInput} and {@link EndianSwitchableOutputStream Output} Streams + * Test for both {@link LittleEndianInputStream EndianSwitchableInput} and {@link LittleEndianOutputStream Output} Streams */ public class EndianSwitchableStreamTest { @Test @@ -41,12 +40,14 @@ public void testWriteLEUnsignedShort() throws IOException { int unsigned = Short.MAX_VALUE + 5; char testChar = 'b'; ByteArrayOutputStream rawOutput = new ByteArrayOutputStream(); - EndianSwitchableOutputStream output = new EndianSwitchableOutputStream(rawOutput, ByteOrder.LITTLE_ENDIAN); + LittleEndianOutputStream output = new LittleEndianOutputStream(rawOutput ); output.writeShort(unsigned); output.writeChar(testChar); + output.close(); - EndianSwitchableInputStream input = new EndianSwitchableInputStream(new ByteArrayInputStream(rawOutput.toByteArray()), ByteOrder.LITTLE_ENDIAN); + LittleEndianInputStream input = new LittleEndianInputStream (new ByteArrayInputStream(rawOutput.toByteArray()) ); assertEquals(unsigned, input.readUnsignedShort()); assertEquals(testChar, input.readChar()); + input.close(); } } diff --git a/src/test/java/com/flowpowered/nbt/stream/NBTInputStreamTest.java b/src/test/java/com/flowpowered/nbt/stream/NBTInputStreamTest.java new file mode 100644 index 0000000..fb0b069 --- /dev/null +++ b/src/test/java/com/flowpowered/nbt/stream/NBTInputStreamTest.java @@ -0,0 +1,26 @@ +package com.flowpowered.nbt.stream; + +import static org.junit.Assert.assertArrayEquals; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Paths; + +import org.junit.Test; + +import com.flowpowered.nbt.Tag; + +public class NBTInputStreamTest { + + /** Read a simple NBT file and compare it to a previous result */ + @Test + public void testNBT() throws IOException, URISyntaxException { + try (NBTInputStream in = new NBTInputStream(getClass().getResourceAsStream("/level.dat"), NBTInputStream.GZIP_COMPRESSION)) { + Tag tag = in.readTag(); + assertArrayEquals( + Files.readAllLines(Paths.get(getClass().getResource("/level.txt").toURI())).toArray(new String[] {}), + tag.toString().split("\r\n")); + } + } +} \ No newline at end of file diff --git a/src/test/resources/level.dat b/src/test/resources/level.dat new file mode 100644 index 0000000..97ea390 Binary files /dev/null and b/src/test/resources/level.dat differ diff --git a/src/test/resources/level.txt b/src/test/resources/level.txt new file mode 100644 index 0000000..61cbf27 --- /dev/null +++ b/src/test/resources/level.txt @@ -0,0 +1,489 @@ +TAG_Compound: 1 entries +{ + TAG_Compound("Data"): 40 entries + { + TAG_Long("RandomSeed"): 4294967295 + TAG_String("generatorName"): default + TAG_Double("BorderCenterZ"): 0.0 + TAG_Byte("Difficulty"): 2 + TAG_Long("BorderSizeLerpTime"): 0 + TAG_Byte("raining"): 0 + TAG_Compound("DimensionData"): 1 entries + { + TAG_Compound("1"): 1 entries + { + TAG_Compound("DragonFight"): 3 entries + { + TAG_List("Gateways"): 20 entries of type TAG_Int + { + TAG_Int: 9 + TAG_Int: 13 + TAG_Int: 6 + TAG_Int: 12 + TAG_Int: 1 + TAG_Int: 8 + TAG_Int: 11 + TAG_Int: 3 + TAG_Int: 4 + TAG_Int: 18 + TAG_Int: 16 + TAG_Int: 19 + TAG_Int: 2 + TAG_Int: 17 + TAG_Int: 0 + TAG_Int: 14 + TAG_Int: 10 + TAG_Int: 7 + TAG_Int: 15 + TAG_Int: 5 + } + TAG_Byte("DragonKilled"): 1 + TAG_Byte("PreviouslyKilled"): 1 + } + } + } + TAG_Long("Time"): 714595 + TAG_Int("GameType"): 0 + TAG_Byte("MapFeatures"): 1 + TAG_Double("BorderCenterX"): 0.0 + TAG_Double("BorderDamagePerBlock"): 0.2 + TAG_Double("BorderWarningBlocks"): 5.0 + TAG_Double("BorderSizeLerpTarget"): 6.0E7 + TAG_Compound("Version"): 3 entries + { + TAG_Byte("Snapshot"): 0 + TAG_Int("Id"): 1519 + TAG_String("Name"): 1.13 + } + TAG_Long("DayTime"): 922064 + TAG_Byte("initialized"): 1 + TAG_Byte("allowCommands"): 1 + TAG_Long("SizeOnDisk"): 0 + TAG_Compound("CustomBossEvents"): 0 entries + { + } + TAG_Compound("GameRules"): 23 entries + { + TAG_String("doTileDrops"): false + TAG_String("doFireTick"): true + TAG_String("maxCommandChainLength"): 65536 + TAG_String("reducedDebugInfo"): false + TAG_String("naturalRegeneration"): true + TAG_String("disableElytraMovementCheck"): false + TAG_String("doMobLoot"): true + TAG_String("announceAdvancements"): true + TAG_String("keepInventory"): false + TAG_String("doEntityDrops"): true + TAG_String("doLimitedCrafting"): false + TAG_String("mobGriefing"): true + TAG_String("randomTickSpeed"): 3 + TAG_String("commandBlockOutput"): true + TAG_String("spawnRadius"): 10 + TAG_String("doMobSpawning"): true + TAG_String("maxEntityCramming"): 24 + TAG_String("logAdminCommands"): true + TAG_String("spectatorsGenerateChunks"): false + TAG_String("doWeatherCycle"): true + TAG_String("sendCommandFeedback"): true + TAG_String("doDaylightCycle"): false + TAG_String("showDeathMessages"): true + } + TAG_Compound("Player"): 43 entries + { + TAG_Int("HurtByTimestamp"): 128850 + TAG_Short("SleepTimer"): 0 + TAG_Byte("SpawnForced"): 0 + TAG_List("Attributes"): 8 entries of type TAG_Compound + { + TAG_Compound: 2 entries + { + TAG_Double("Base"): 20.0 + TAG_String("Name"): generic.maxHealth + } + TAG_Compound: 2 entries + { + TAG_Double("Base"): 0.0 + TAG_String("Name"): generic.knockbackResistance + } + TAG_Compound: 2 entries + { + TAG_Double("Base"): 0.10000000149011612 + TAG_String("Name"): generic.movementSpeed + } + TAG_Compound: 2 entries + { + TAG_Double("Base"): 0.0 + TAG_String("Name"): generic.armor + } + TAG_Compound: 2 entries + { + TAG_Double("Base"): 0.0 + TAG_String("Name"): generic.armorToughness + } + TAG_Compound: 2 entries + { + TAG_Double("Base"): 1.0 + TAG_String("Name"): generic.attackDamage + } + TAG_Compound: 2 entries + { + TAG_Double("Base"): 4.0 + TAG_String("Name"): generic.attackSpeed + } + TAG_Compound: 2 entries + { + TAG_Double("Base"): 0.0 + TAG_String("Name"): generic.luck + } + } + TAG_Byte("Invulnerable"): 0 + TAG_Byte("FallFlying"): 0 + TAG_Int("PortalCooldown"): 0 + TAG_Float("AbsorptionAmount"): 0.0 + TAG_Compound("abilities"): 7 entries + { + TAG_Byte("invulnerable"): 1 + TAG_Byte("mayfly"): 1 + TAG_Byte("instabuild"): 1 + TAG_Float("walkSpeed"): 0.1 + TAG_Byte("mayBuild"): 1 + TAG_Byte("flying"): 1 + TAG_Float("flySpeed"): 0.05 + } + TAG_Float("FallDistance"): 0.0 + TAG_Compound("recipeBook"): 6 entries + { + TAG_List("recipes"): 42 entries of type TAG_String + { + TAG_String: minecraft:activator_rail + TAG_String: minecraft:cobblestone_stairs + TAG_String: minecraft:stone_shovel + TAG_String: minecraft:chest + TAG_String: minecraft:crafting_table + TAG_String: minecraft:spruce_stairs + TAG_String: minecraft:stone + TAG_String: minecraft:powered_rail + TAG_String: minecraft:cobblestone_wall + TAG_String: minecraft:charcoal + TAG_String: minecraft:oak_planks + TAG_String: minecraft:stone_hoe + TAG_String: minecraft:oak_button + TAG_String: minecraft:oak_fence + TAG_String: minecraft:lever + TAG_String: minecraft:oak_stairs + TAG_String: minecraft:stone_pressure_plate + TAG_String: minecraft:oak_pressure_plate + TAG_String: minecraft:stone_sword + TAG_String: minecraft:stone_button + TAG_String: minecraft:stone_pickaxe + TAG_String: minecraft:spruce_trapdoor + TAG_String: minecraft:spruce_fence_gate + TAG_String: minecraft:spruce_fence + TAG_String: minecraft:stick + TAG_String: minecraft:cobblestone_slab + TAG_String: minecraft:spruce_door + TAG_String: minecraft:detector_rail + TAG_String: minecraft:oak_fence_gate + TAG_String: minecraft:oak_slab + TAG_String: minecraft:furnace + TAG_String: minecraft:stone_bricks + TAG_String: minecraft:stone_slab + TAG_String: minecraft:spruce_slab + TAG_String: minecraft:spruce_button + TAG_String: minecraft:oak_trapdoor + TAG_String: minecraft:oak_door + TAG_String: minecraft:sign + TAG_String: minecraft:stone_axe + TAG_String: minecraft:spruce_pressure_plate + TAG_String: minecraft:trapped_chest + TAG_String: minecraft:oak_wood + } + TAG_Byte("isFilteringCraftable"): 0 + TAG_List("toBeDisplayed"): 42 entries of type TAG_String + { + TAG_String: minecraft:activator_rail + TAG_String: minecraft:cobblestone_stairs + TAG_String: minecraft:stone_shovel + TAG_String: minecraft:chest + TAG_String: minecraft:crafting_table + TAG_String: minecraft:spruce_stairs + TAG_String: minecraft:stone + TAG_String: minecraft:powered_rail + TAG_String: minecraft:cobblestone_wall + TAG_String: minecraft:charcoal + TAG_String: minecraft:oak_planks + TAG_String: minecraft:stone_hoe + TAG_String: minecraft:oak_button + TAG_String: minecraft:oak_fence + TAG_String: minecraft:lever + TAG_String: minecraft:oak_stairs + TAG_String: minecraft:stone_pressure_plate + TAG_String: minecraft:oak_pressure_plate + TAG_String: minecraft:stone_sword + TAG_String: minecraft:stone_button + TAG_String: minecraft:stone_pickaxe + TAG_String: minecraft:spruce_trapdoor + TAG_String: minecraft:spruce_fence_gate + TAG_String: minecraft:spruce_fence + TAG_String: minecraft:stick + TAG_String: minecraft:cobblestone_slab + TAG_String: minecraft:spruce_door + TAG_String: minecraft:detector_rail + TAG_String: minecraft:oak_fence_gate + TAG_String: minecraft:oak_slab + TAG_String: minecraft:furnace + TAG_String: minecraft:stone_bricks + TAG_String: minecraft:stone_slab + TAG_String: minecraft:spruce_slab + TAG_String: minecraft:spruce_button + TAG_String: minecraft:oak_trapdoor + TAG_String: minecraft:oak_door + TAG_String: minecraft:sign + TAG_String: minecraft:stone_axe + TAG_String: minecraft:spruce_pressure_plate + TAG_String: minecraft:trapped_chest + TAG_String: minecraft:oak_wood + } + TAG_Byte("isFurnaceGuiOpen"): 0 + TAG_Byte("isGuiOpen"): 0 + TAG_Byte("isFurnaceFilteringCraftable"): 0 + } + TAG_Short("DeathTime"): 0 + TAG_Int("XpSeed"): -962796252 + TAG_Int("XpTotal"): 733 + TAG_Int("playerGameType"): 1 + TAG_Byte("seenCredits"): 0 + TAG_List("Motion"): 3 entries of type TAG_Double + { + TAG_Double: 0.0 + TAG_Double: 0.0 + TAG_Double: 0.0 + } + TAG_Int("SpawnY"): 82 + TAG_Long("UUIDLeast"): -7307225268105228908 + TAG_Float("Health"): 20.0 + TAG_Int("SpawnZ"): 2236 + TAG_Float("foodSaturationLevel"): 18.0 + TAG_Int("SpawnX"): -4048 + TAG_Short("Air"): 300 + TAG_Byte("OnGround"): 0 + TAG_Int("Dimension"): 0 + TAG_List("Rotation"): 2 entries of type TAG_Float + { + TAG_Float: 16.425446 + TAG_Float: 90.0 + } + TAG_Int("XpLevel"): 22 + TAG_Int("Score"): 733 + TAG_Long("UUIDMost"): -6620843258805795274 + TAG_Byte("Sleeping"): 0 + TAG_List("Pos"): 3 entries of type TAG_Double + { + TAG_Double: 508.30314538488165 + TAG_Double: 78.19136852275403 + TAG_Double: 2035.9523347535592 + } + TAG_Short("Fire"): -20 + TAG_Float("XpP"): 0.74999964 + TAG_List("EnderItems"): 0 entries of type TAG_End + { + } + TAG_Int("DataVersion"): 1519 + TAG_Int("foodLevel"): 20 + TAG_Float("foodExhaustionLevel"): 3.953829 + TAG_Short("HurtTime"): 0 + TAG_Int("SelectedItemSlot"): 5 + TAG_List("ActiveEffects"): 1 entries of type TAG_Compound + { + TAG_Compound: 6 entries + { + TAG_Byte("Ambient"): 0 + TAG_Byte("ShowIcon"): 1 + TAG_Byte("ShowParticles"): 1 + TAG_Int("Duration"): 1949261 + TAG_Byte("Id"): 16 + TAG_Byte("Amplifier"): 0 + } + } + TAG_List("Inventory"): 24 entries of type TAG_Compound + { + TAG_Compound: 3 entries + { + TAG_Byte("Slot"): 0 + TAG_String("id"): minecraft:dirt + TAG_Byte("Count"): 1 + } + TAG_Compound: 3 entries + { + TAG_Byte("Slot"): 1 + TAG_String("id"): minecraft:cobblestone + TAG_Byte("Count"): 1 + } + TAG_Compound: 3 entries + { + TAG_Byte("Slot"): 2 + TAG_String("id"): minecraft:stone + TAG_Byte("Count"): 1 + } + TAG_Compound: 3 entries + { + TAG_Byte("Slot"): 3 + TAG_String("id"): minecraft:oak_log + TAG_Byte("Count"): 1 + } + TAG_Compound: 3 entries + { + TAG_Byte("Slot"): 4 + TAG_String("id"): minecraft:oak_planks + TAG_Byte("Count"): 1 + } + TAG_Compound: 3 entries + { + TAG_Byte("Slot"): 5 + TAG_String("id"): minecraft:hopper + TAG_Byte("Count"): 1 + } + TAG_Compound: 3 entries + { + TAG_Byte("Slot"): 6 + TAG_String("id"): minecraft:chest + TAG_Byte("Count"): 1 + } + TAG_Compound: 3 entries + { + TAG_Byte("Slot"): 7 + TAG_String("id"): minecraft:spruce_planks + TAG_Byte("Count"): 1 + } + TAG_Compound: 3 entries + { + TAG_Byte("Slot"): 8 + TAG_String("id"): minecraft:polished_andesite + TAG_Byte("Count"): 1 + } + TAG_Compound: 3 entries + { + TAG_Byte("Slot"): 9 + TAG_String("id"): minecraft:dirt + TAG_Byte("Count"): 1 + } + TAG_Compound: 3 entries + { + TAG_Byte("Slot"): 10 + TAG_String("id"): minecraft:powered_rail + TAG_Byte("Count"): 1 + } + TAG_Compound: 3 entries + { + TAG_Byte("Slot"): 11 + TAG_String("id"): minecraft:spruce_slab + TAG_Byte("Count"): 1 + } + TAG_Compound: 3 entries + { + TAG_Byte("Slot"): 12 + TAG_String("id"): minecraft:tripwire_hook + TAG_Byte("Count"): 1 + } + TAG_Compound: 3 entries + { + TAG_Byte("Slot"): 13 + TAG_String("id"): minecraft:bookshelf + TAG_Byte("Count"): 1 + } + TAG_Compound: 3 entries + { + TAG_Byte("Slot"): 14 + TAG_String("id"): minecraft:stone_pressure_plate + TAG_Byte("Count"): 1 + } + TAG_Compound: 4 entries + { + TAG_Byte("Slot"): 15 + TAG_String("id"): minecraft:flint_and_steel + TAG_Byte("Count"): 1 + TAG_Compound("tag"): 1 entries + { + TAG_Int("Damage"): 0 + } + } + TAG_Compound: 3 entries + { + TAG_Byte("Slot"): 19 + TAG_String("id"): minecraft:cobblestone + TAG_Byte("Count"): 10 + } + TAG_Compound: 3 entries + { + TAG_Byte("Slot"): 20 + TAG_String("id"): minecraft:oak_log + TAG_Byte("Count"): 10 + } + TAG_Compound: 3 entries + { + TAG_Byte("Slot"): 28 + TAG_String("id"): minecraft:cobblestone_slab + TAG_Byte("Count"): 1 + } + TAG_Compound: 3 entries + { + TAG_Byte("Slot"): 31 + TAG_String("id"): minecraft:torch + TAG_Byte("Count"): 1 + } + TAG_Compound: 3 entries + { + TAG_Byte("Slot"): 32 + TAG_String("id"): minecraft:furnace + TAG_Byte("Count"): 1 + } + TAG_Compound: 3 entries + { + TAG_Byte("Slot"): 33 + TAG_String("id"): minecraft:polished_andesite + TAG_Byte("Count"): 1 + } + TAG_Compound: 3 entries + { + TAG_Byte("Slot"): 34 + TAG_String("id"): minecraft:rail + TAG_Byte("Count"): 1 + } + TAG_Compound: 3 entries + { + TAG_Byte("Slot"): 35 + TAG_String("id"): minecraft:tnt + TAG_Byte("Count"): 1 + } + } + TAG_Int("foodTickTimer"): 0 + } + TAG_Int("SpawnY"): 64 + TAG_Int("rainTime"): 16093 + TAG_Int("thunderTime"): 17595 + TAG_Int("SpawnZ"): 256 + TAG_Byte("hardcore"): 0 + TAG_Byte("DifficultyLocked"): 0 + TAG_Int("SpawnX"): 128 + TAG_Int("clearWeatherTime"): 0 + TAG_Byte("thundering"): 0 + TAG_Int("generatorVersion"): 1 + TAG_Int("version"): 19133 + TAG_Double("BorderSafeZone"): 5.0 + TAG_Long("LastPlayed"): 1534516348794 + TAG_Double("BorderWarningTime"): 15.0 + TAG_String("LevelName"): Multi 5 + TAG_Double("BorderSize"): 6.0E7 + TAG_Int("DataVersion"): 1519 + TAG_Compound("DataPacks"): 2 entries + { + TAG_List("Enabled"): 1 entries of type TAG_String + { + TAG_String: vanilla + } + TAG_List("Disabled"): 0 entries of type TAG_End + { + } + } + } +} diff --git a/src/test/resources/pre-1.13.mca b/src/test/resources/pre-1.13.mca new file mode 100755 index 0000000..903f501 Binary files /dev/null and b/src/test/resources/pre-1.13.mca differ diff --git a/src/test/resources/r.0.0.mca b/src/test/resources/r.0.0.mca new file mode 100644 index 0000000..afe07d4 Binary files /dev/null and b/src/test/resources/r.0.0.mca differ diff --git a/src/test/resources/r.1.3.mca b/src/test/resources/r.1.3.mca new file mode 100755 index 0000000..1bac8d2 Binary files /dev/null and b/src/test/resources/r.1.3.mca differ