diff --git a/build.gradle b/build.gradle
index 4d457e77..4e79db37 100644
--- a/build.gradle
+++ b/build.gradle
@@ -10,7 +10,7 @@ apply plugin: 'jacoco'
group = 'net.querz.nbt'
archivesBaseName = 'nbt'
-version = '5.5'
+version = '6.0'
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
compileJava.options.encoding = 'UTF-8'
diff --git a/src/main/java/net/querz/mca/Chunk.java b/src/main/java/net/querz/mca/Chunk.java
index d305df04..cfe06ce6 100644
--- a/src/main/java/net/querz/mca/Chunk.java
+++ b/src/main/java/net/querz/mca/Chunk.java
@@ -16,9 +16,10 @@
public class Chunk {
- public static final int DEFAULT_DATA_VERSION = 1628;
+ public static final int DEFAULT_DATA_VERSION = 2567;
private boolean partial;
+ private boolean raw;
private int lastMCAUpdate;
@@ -59,6 +60,12 @@ private void initReferences(long loadFlags) {
if (data == null) {
throw new NullPointerException("data cannot be null");
}
+
+ if ((loadFlags != ALL_DATA) && (loadFlags & RAW) != 0) {
+ raw = true;
+ return;
+ }
+
CompoundTag level;
if ((level = data.getCompoundTag("Level")) == null) {
throw new IllegalArgumentException("data does not contain \"Level\" tag");
@@ -121,8 +128,6 @@ private void initReferences(long loadFlags) {
if (loadFlags != ALL_DATA) {
data = null;
partial = true;
- } else {
- partial = false;
}
}
@@ -224,6 +229,7 @@ public int getBiomeAt(int blockX, int blockY, int blockZ) {
@Deprecated
public void setBiomeAt(int blockX, int blockZ, int biomeID) {
+ checkRaw();
if (dataVersion < 2202) {
if (biomes == null || biomes.length != 256) {
biomes = new int[256];
@@ -254,6 +260,7 @@ public void setBiomeAt(int blockX, int blockZ, int biomeID) {
* When set to a negative number, Minecraft will replace it with the block column's default biome.
*/
public void setBiomeAt(int blockX, int blockY, int blockZ, int biomeID) {
+ checkRaw();
if (dataVersion < 2202) {
if (biomes == null || biomes.length != 256) {
biomes = new int[256];
@@ -274,7 +281,7 @@ public void setBiomeAt(int blockX, int blockY, int blockZ, int biomeID) {
}
int getBiomeIndex(int biomeX, int biomeY, int biomeZ) {
- return biomeY * 64 + biomeZ * 4 + biomeX;
+ return biomeY * 16 + biomeZ * 4 + biomeX;
}
public CompoundTag getBlockStateAt(int blockX, int blockY, int blockZ) {
@@ -297,6 +304,7 @@ public CompoundTag getBlockStateAt(int blockX, int blockY, int blockZ) {
* Recalculating the Palette should only be executed once right before saving the Chunk to file.
*/
public void setBlockStateAt(int blockX, int blockY, int blockZ, CompoundTag state, boolean cleanup) {
+ checkRaw();
int sectionIndex = MCAUtil.blockToChunk(blockY);
Section section = sections[sectionIndex];
if (section == null) {
@@ -318,6 +326,7 @@ public int getDataVersion() {
* @param dataVersion The DataVersion to be set.
*/
public void setDataVersion(int dataVersion) {
+ checkRaw();
this.dataVersion = dataVersion;
}
@@ -333,6 +342,7 @@ public int getLastMCAUpdate() {
* @param lastMCAUpdate The time in seconds since 1970-01-01.
*/
public void setLastMCAUpdate(int lastMCAUpdate) {
+ checkRaw();
this.lastMCAUpdate = lastMCAUpdate;
}
@@ -348,6 +358,7 @@ public String getStatus() {
* @param status The generation status of this chunk.
*/
public void setStatus(String status) {
+ checkRaw();
this.status = status;
}
@@ -366,6 +377,7 @@ public Section getSection(int sectionY) {
* @param section The section to be set.
*/
public void setSection(int sectionY, Section section) {
+ checkRaw();
sections[sectionY] = section;
}
@@ -381,6 +393,7 @@ public long getLastUpdate() {
* @param lastUpdate The UNIX timestamp.
*/
public void setLastUpdate(long lastUpdate) {
+ checkRaw();
this.lastUpdate = lastUpdate;
}
@@ -396,6 +409,7 @@ public long getInhabitedTime() {
* @param inhabitedTime The time in ticks.
*/
public void setInhabitedTime(long inhabitedTime) {
+ checkRaw();
this.inhabitedTime = inhabitedTime;
}
@@ -413,6 +427,7 @@ public int[] getBiomes() {
* or is null
*/
public void setBiomes(int[] biomes) {
+ checkRaw();
if (biomes != null) {
if (dataVersion < 2202 && biomes.length != 256 || dataVersion >= 2202 && biomes.length != 1024) {
throw new IllegalArgumentException("biomes array must have a length of " + (dataVersion < 2202 ? "256" : "1024"));
@@ -433,6 +448,7 @@ public CompoundTag getHeightMaps() {
* @param heightMaps The height maps.
*/
public void setHeightMaps(CompoundTag heightMaps) {
+ checkRaw();
this.heightMaps = heightMaps;
}
@@ -448,6 +464,7 @@ public CompoundTag getCarvingMasks() {
* @param carvingMasks The carving masks.
*/
public void setCarvingMasks(CompoundTag carvingMasks) {
+ checkRaw();
this.carvingMasks = carvingMasks;
}
@@ -463,6 +480,7 @@ public ListTag getEntities() {
* @param entities The entities.
*/
public void setEntities(ListTag entities) {
+ checkRaw();
this.entities = entities;
}
@@ -478,6 +496,7 @@ public ListTag getTileEntities() {
* @param tileEntities The tile entities of this chunk.
*/
public void setTileEntities(ListTag tileEntities) {
+ checkRaw();
this.tileEntities = tileEntities;
}
@@ -493,6 +512,7 @@ public ListTag getTileTicks() {
* @param tileTicks Thee tile ticks.
*/
public void setTileTicks(ListTag tileTicks) {
+ checkRaw();
this.tileTicks = tileTicks;
}
@@ -508,6 +528,7 @@ public ListTag getLiquidTicks() {
* @param liquidTicks The liquid ticks.
*/
public void setLiquidTicks(ListTag liquidTicks) {
+ checkRaw();
this.liquidTicks = liquidTicks;
}
@@ -523,11 +544,12 @@ public ListTag> getLights() {
* @param lights The light sources.
*/
public void setLights(ListTag> lights) {
+ checkRaw();
this.lights = lights;
}
/**
- * @return THe liquids to be ticked in this chunk.
+ * @return The liquids to be ticked in this chunk.
*/
public ListTag> getLiquidsToBeTicked() {
return liquidsToBeTicked;
@@ -538,6 +560,7 @@ public ListTag> getLiquidsToBeTicked() {
* @param liquidsToBeTicked The liquids to be ticked.
*/
public void setLiquidsToBeTicked(ListTag> liquidsToBeTicked) {
+ checkRaw();
this.liquidsToBeTicked = liquidsToBeTicked;
}
@@ -553,6 +576,7 @@ public ListTag> getToBeTicked() {
* @param toBeTicked The stuff to be ticked.
*/
public void setToBeTicked(ListTag> toBeTicked) {
+ checkRaw();
this.toBeTicked = toBeTicked;
}
@@ -568,6 +592,7 @@ public ListTag> getPostProcessing() {
* @param postProcessing The things to be post processed.
*/
public void setPostProcessing(ListTag> postProcessing) {
+ checkRaw();
this.postProcessing = postProcessing;
}
@@ -583,6 +608,7 @@ public CompoundTag getStructures() {
* @param structures The data about structures.
*/
public void setStructures(CompoundTag structures) {
+ checkRaw();
this.structures = structures;
}
@@ -591,6 +617,7 @@ int getBlockIndex(int blockX, int blockZ) {
}
public void cleanupPalettesAndBlockStates() {
+ checkRaw();
for (Section section : sections) {
if (section != null) {
section.cleanupPaletteAndBlockStates();
@@ -598,16 +625,38 @@ public void cleanupPalettesAndBlockStates() {
}
}
+ private void checkRaw() {
+ if (raw) {
+ throw new UnsupportedOperationException("cannot update field when working with raw data");
+ }
+ }
+
public static Chunk newChunk() {
+ return newChunk(DEFAULT_DATA_VERSION);
+ }
+
+ public static Chunk newChunk(int dataVersion) {
Chunk c = new Chunk(0);
- c.dataVersion = DEFAULT_DATA_VERSION;
+ c.dataVersion = dataVersion;
c.data = new CompoundTag();
c.data.put("Level", new CompoundTag());
c.status = "mobs_spawned";
return c;
}
+ /**
+ * Provides a reference to the full chunk data.
+ * @return The full chunk data or null if there is none, e.g. when this chunk has only been loaded partially.
+ */
+ public CompoundTag getHandle() {
+ return data;
+ }
+
public CompoundTag updateHandle(int xPos, int zPos) {
+ if (raw) {
+ return data;
+ }
+
data.putInt("DataVersion", dataVersion);
CompoundTag level = data.getCompoundTag("Level");
level.putInt("xPos", xPos);
@@ -615,22 +664,48 @@ public CompoundTag updateHandle(int xPos, int zPos) {
level.putLong("LastUpdate", lastUpdate);
level.putLong("InhabitedTime", inhabitedTime);
if (dataVersion < 2202) {
- if (biomes != null && biomes.length == 256) level.putIntArray("Biomes", biomes);
+ if (biomes != null && biomes.length == 256) {
+ level.putIntArray("Biomes", biomes);
+ }
} else {
- if (biomes != null && biomes.length == 1024) level.putIntArray("Biomes", biomes);
- }
- if (heightMaps != null) level.put("Heightmaps", heightMaps);
- if (carvingMasks != null) level.put("CarvingMasks", carvingMasks);
- if (entities != null) level.put("Entities", entities);
- if (tileEntities != null) level.put("TileEntities", tileEntities);
- if (tileTicks != null) level.put("TileTicks", tileTicks);
- if (liquidTicks != null) level.put("LiquidTicks", liquidTicks);
- if (lights != null) level.put("Lights", lights);
- if (liquidsToBeTicked != null) level.put("LiquidsToBeTicked", liquidsToBeTicked);
- if (toBeTicked != null) level.put("ToBeTicked", toBeTicked);
- if (postProcessing != null) level.put("PostProcessing", postProcessing);
+ if (biomes != null && biomes.length == 1024) {
+ level.putIntArray("Biomes", biomes);
+ }
+ }
+ if (heightMaps != null) {
+ level.put("Heightmaps", heightMaps);
+ }
+ if (carvingMasks != null) {
+ level.put("CarvingMasks", carvingMasks);
+ }
+ if (entities != null) {
+ level.put("Entities", entities);
+ }
+ if (tileEntities != null) {
+ level.put("TileEntities", tileEntities);
+ }
+ if (tileTicks != null) {
+ level.put("TileTicks", tileTicks);
+ }
+ if (liquidTicks != null) {
+ level.put("LiquidTicks", liquidTicks);
+ }
+ if (lights != null) {
+ level.put("Lights", lights);
+ }
+ if (liquidsToBeTicked != null) {
+ level.put("LiquidsToBeTicked", liquidsToBeTicked);
+ }
+ if (toBeTicked != null) {
+ level.put("ToBeTicked", toBeTicked);
+ }
+ if (postProcessing != null) {
+ level.put("PostProcessing", postProcessing);
+ }
level.putString("Status", status);
- if (structures != null) level.put("Structures", structures);
+ if (structures != null) {
+ level.put("Structures", structures);
+ }
ListTag sections = new ListTag<>(CompoundTag.class);
for (int i = 0; i < this.sections.length; i++) {
if (this.sections[i] != null) {
diff --git a/src/main/java/net/querz/mca/LoadFlags.java b/src/main/java/net/querz/mca/LoadFlags.java
index e4122898..da37a597 100644
--- a/src/main/java/net/querz/mca/LoadFlags.java
+++ b/src/main/java/net/querz/mca/LoadFlags.java
@@ -1,24 +1,23 @@
package net.querz.mca;
-public class LoadFlags {
-
- public static long BIOMES = 0x0001;
- public static long HEIGHTMAPS = 0x0002;
- public static long CARVING_MASKS = 0x0004;
- public static long ENTITIES = 0x0008;
- public static long TILE_ENTITIES = 0x0010;
- public static long TILE_TICKS = 0x0040;
- public static long LIQUID_TICKS = 0x0080;
- public static long TO_BE_TICKED = 0x0100;
- public static long POST_PROCESSING = 0x0200;
- public static long STRUCTURES = 0x0400;
- public static long BLOCK_LIGHTS = 0x0800;
- public static long BLOCK_STATES = 0x1000;
- public static long SKY_LIGHT = 0x2000;
- public static long LIGHTS = 0x4000;
- public static long LIQUIDS_TO_BE_TICKED = 0x8000;
-
- public static long ALL_DATA = 0xffffffffffffffffL;
+public final class LoadFlags {
+ public static final long BIOMES = 0x00001;
+ public static final long HEIGHTMAPS = 0x00002;
+ public static final long CARVING_MASKS = 0x00004;
+ public static final long ENTITIES = 0x00008;
+ public static final long TILE_ENTITIES = 0x00010;
+ public static final long TILE_TICKS = 0x00040;
+ public static final long LIQUID_TICKS = 0x00080;
+ public static final long TO_BE_TICKED = 0x00100;
+ public static final long POST_PROCESSING = 0x00200;
+ public static final long STRUCTURES = 0x00400;
+ public static final long BLOCK_LIGHTS = 0x00800;
+ public static final long BLOCK_STATES = 0x01000;
+ public static final long SKY_LIGHT = 0x02000;
+ public static final long LIGHTS = 0x04000;
+ public static final long LIQUIDS_TO_BE_TICKED = 0x08000;
+ public static final long RAW = 0x10000;
+ public static final long ALL_DATA = 0xffffffffffffffffL;
}
diff --git a/src/main/java/net/querz/nbt/io/LittleEndianNBTInputStream.java b/src/main/java/net/querz/nbt/io/LittleEndianNBTInputStream.java
new file mode 100644
index 00000000..7c7d8ed2
--- /dev/null
+++ b/src/main/java/net/querz/nbt/io/LittleEndianNBTInputStream.java
@@ -0,0 +1,240 @@
+package net.querz.nbt.io;
+
+import net.querz.io.ExceptionBiFunction;
+import net.querz.io.MaxDepthIO;
+import net.querz.nbt.tag.ByteArrayTag;
+import net.querz.nbt.tag.ByteTag;
+import net.querz.nbt.tag.CompoundTag;
+import net.querz.nbt.tag.DoubleTag;
+import net.querz.nbt.tag.EndTag;
+import net.querz.nbt.tag.FloatTag;
+import net.querz.nbt.tag.IntArrayTag;
+import net.querz.nbt.tag.IntTag;
+import net.querz.nbt.tag.ListTag;
+import net.querz.nbt.tag.LongArrayTag;
+import net.querz.nbt.tag.LongTag;
+import net.querz.nbt.tag.ShortTag;
+import net.querz.nbt.tag.StringTag;
+import net.querz.nbt.tag.Tag;
+import java.io.Closeable;
+import java.io.DataInput;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+public class LittleEndianNBTInputStream implements DataInput, NBTInput, MaxDepthIO, Closeable {
+
+ private final DataInputStream input;
+
+ private static Map, IOException>> readers = new HashMap<>();
+ private static Map> idClassMapping = new HashMap<>();
+
+ static {
+ put(EndTag.ID, (i, d) -> EndTag.INSTANCE, EndTag.class);
+ put(ByteTag.ID, (i, d) -> readByte(i), ByteTag.class);
+ put(ShortTag.ID, (i, d) -> readShort(i), ShortTag.class);
+ put(IntTag.ID, (i, d) -> readInt(i), IntTag.class);
+ put(LongTag.ID, (i, d) -> readLong(i), LongTag.class);
+ put(FloatTag.ID, (i, d) -> readFloat(i), FloatTag.class);
+ put(DoubleTag.ID, (i, d) -> readDouble(i), DoubleTag.class);
+ put(ByteArrayTag.ID, (i, d) -> readByteArray(i), ByteArrayTag.class);
+ put(StringTag.ID, (i, d) -> readString(i), StringTag.class);
+ put(ListTag.ID, LittleEndianNBTInputStream::readListTag, ListTag.class);
+ put(CompoundTag.ID, LittleEndianNBTInputStream::readCompound, CompoundTag.class);
+ put(IntArrayTag.ID, (i, d) -> readIntArray(i), IntArrayTag.class);
+ put(LongArrayTag.ID, (i, d) -> readLongArray(i), LongArrayTag.class);
+ }
+
+ private static void put(byte id, ExceptionBiFunction, IOException> reader, Class> clazz) {
+ readers.put(id, reader);
+ idClassMapping.put(id, clazz);
+ }
+
+ public LittleEndianNBTInputStream(InputStream in) {
+ input = new DataInputStream(in);
+ }
+
+ public LittleEndianNBTInputStream(DataInputStream in) {
+ input = in;
+ }
+
+ public NamedTag readTag(int maxDepth) throws IOException {
+ byte id = readByte();
+ return new NamedTag(readUTF(), readTag(id, maxDepth));
+ }
+
+ public Tag> readRawTag(int maxDepth) throws IOException {
+ byte id = readByte();
+ return readTag(id, maxDepth);
+ }
+
+ private Tag> readTag(byte type, int maxDepth) throws IOException {
+ ExceptionBiFunction, IOException> f;
+ if ((f = readers.get(type)) == null) {
+ throw new IOException("invalid tag id \"" + type + "\"");
+ }
+ return f.accept(this, maxDepth);
+ }
+
+ private static ByteTag readByte(LittleEndianNBTInputStream in) throws IOException {
+ return new ByteTag(in.readByte());
+ }
+
+ private static ShortTag readShort(LittleEndianNBTInputStream in) throws IOException {
+ return new ShortTag(in.readShort());
+ }
+
+ private static IntTag readInt(LittleEndianNBTInputStream in) throws IOException {
+ return new IntTag(in.readInt());
+ }
+
+ private static LongTag readLong(LittleEndianNBTInputStream in) throws IOException {
+ return new LongTag(in.readLong());
+ }
+
+ private static FloatTag readFloat(LittleEndianNBTInputStream in) throws IOException {
+ return new FloatTag(in.readFloat());
+ }
+
+ private static DoubleTag readDouble(LittleEndianNBTInputStream in) throws IOException {
+ return new DoubleTag(in.readDouble());
+ }
+
+ private static StringTag readString(LittleEndianNBTInputStream in) throws IOException {
+ return new StringTag(in.readUTF());
+ }
+
+ private static ByteArrayTag readByteArray(LittleEndianNBTInputStream in) throws IOException {
+ ByteArrayTag bat = new ByteArrayTag(new byte[in.readInt()]);
+ in.readFully(bat.getValue());
+ return bat;
+ }
+
+ private static IntArrayTag readIntArray(LittleEndianNBTInputStream in) throws IOException {
+ int l = in.readInt();
+ int[] data = new int[l];
+ IntArrayTag iat = new IntArrayTag(data);
+ for (int i = 0; i < l; i++) {
+ data[i] = in.readInt();
+ }
+ return iat;
+ }
+
+ private static LongArrayTag readLongArray(LittleEndianNBTInputStream in) throws IOException {
+ int l = in.readInt();
+ long[] data = new long[l];
+ LongArrayTag iat = new LongArrayTag(data);
+ for (int i = 0; i < l; i++) {
+ data[i] = in.readLong();
+ }
+ return iat;
+ }
+
+ private static ListTag> readListTag(LittleEndianNBTInputStream in, int maxDepth) throws IOException {
+ byte listType = in.readByte();
+ ListTag> list = ListTag.createUnchecked(idClassMapping.get(listType));
+ int length = in.readInt();
+ if (length < 0) {
+ length = 0;
+ }
+ for (int i = 0; i < length; i++) {
+ list.addUnchecked(in.readTag(listType, in.decrementMaxDepth(maxDepth)));
+ }
+ return list;
+ }
+
+ private static CompoundTag readCompound(LittleEndianNBTInputStream in, int maxDepth) throws IOException {
+ CompoundTag comp = new CompoundTag();
+ for (int id = in.readByte() & 0xFF; id != 0; id = in.readByte() & 0xFF) {
+ String key = in.readUTF();
+ Tag> element = in.readTag((byte) id, in.decrementMaxDepth(maxDepth));
+ comp.put(key, element);
+ }
+ return comp;
+ }
+
+ @Override
+ public void readFully(byte[] b) throws IOException {
+ input.readFully(b);
+ }
+
+ @Override
+ public void readFully(byte[] b, int off, int len) throws IOException {
+ input.readFully(b, off, len);
+ }
+
+ @Override
+ public int skipBytes(int n) throws IOException {
+ return input.skipBytes(n);
+ }
+
+ @Override
+ public boolean readBoolean() throws IOException {
+ return input.readBoolean();
+ }
+
+ @Override
+ public byte readByte() throws IOException {
+ return input.readByte();
+ }
+
+ @Override
+ public int readUnsignedByte() throws IOException {
+ return input.readUnsignedByte();
+ }
+
+ @Override
+ public short readShort() throws IOException {
+ return Short.reverseBytes(input.readShort());
+ }
+
+ public int readUnsignedShort() throws IOException {
+ return Short.toUnsignedInt(Short.reverseBytes(input.readShort()));
+ }
+
+ @Override
+ public char readChar() throws IOException {
+ return Character.reverseBytes(input.readChar());
+ }
+
+ @Override
+ public int readInt() throws IOException {
+ return Integer.reverseBytes(input.readInt());
+ }
+
+ @Override
+ public long readLong() throws IOException {
+ return Long.reverseBytes(input.readLong());
+ }
+
+ @Override
+ public float readFloat() throws IOException {
+ return Float.intBitsToFloat(Integer.reverseBytes(input.readInt()));
+ }
+
+ @Override
+ public double readDouble() throws IOException {
+ return Double.longBitsToDouble(Long.reverseBytes(input.readLong()));
+ }
+
+ @Override
+ @Deprecated
+ public String readLine() throws IOException {
+ return input.readLine();
+ }
+
+ @Override
+ public void close() throws IOException {
+ input.close();
+ }
+
+ @Override
+ public String readUTF() throws IOException {
+ byte[] bytes = new byte[readUnsignedShort()];
+ readFully(bytes);
+ return new String(bytes, StandardCharsets.UTF_8);
+ }
+}
diff --git a/src/main/java/net/querz/nbt/io/LittleEndianNBTOutputStream.java b/src/main/java/net/querz/nbt/io/LittleEndianNBTOutputStream.java
new file mode 100644
index 00000000..9cda01b2
--- /dev/null
+++ b/src/main/java/net/querz/nbt/io/LittleEndianNBTOutputStream.java
@@ -0,0 +1,244 @@
+package net.querz.nbt.io;
+
+import net.querz.io.ExceptionTriConsumer;
+import net.querz.io.MaxDepthIO;
+import net.querz.nbt.tag.ByteArrayTag;
+import net.querz.nbt.tag.ByteTag;
+import net.querz.nbt.tag.CompoundTag;
+import net.querz.nbt.tag.DoubleTag;
+import net.querz.nbt.tag.EndTag;
+import net.querz.nbt.tag.FloatTag;
+import net.querz.nbt.tag.IntArrayTag;
+import net.querz.nbt.tag.IntTag;
+import net.querz.nbt.tag.ListTag;
+import net.querz.nbt.tag.LongArrayTag;
+import net.querz.nbt.tag.LongTag;
+import net.querz.nbt.tag.ShortTag;
+import net.querz.nbt.tag.StringTag;
+import net.querz.nbt.tag.Tag;
+import java.io.Closeable;
+import java.io.DataOutput;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+public class LittleEndianNBTOutputStream implements DataOutput, NBTOutput, MaxDepthIO, Closeable {
+
+ private final DataOutputStream output;
+
+ private static Map, Integer, IOException>> writers = new HashMap<>();
+ private static Map, Byte> classIdMapping = new HashMap<>();
+
+ static {
+ put(EndTag.ID, (o, t, d) -> {}, EndTag.class);
+ put(ByteTag.ID, (o, t, d) -> writeByte(o, t), ByteTag.class);
+ put(ShortTag.ID, (o, t, d) -> writeShort(o, t), ShortTag.class);
+ put(IntTag.ID, (o, t, d) -> writeInt(o, t), IntTag.class);
+ put(LongTag.ID, (o, t, d) -> writeLong(o, t), LongTag.class);
+ put(FloatTag.ID, (o, t, d) -> writeFloat(o, t), FloatTag.class);
+ put(DoubleTag.ID, (o, t, d) -> writeDouble(o, t), DoubleTag.class);
+ put(ByteArrayTag.ID, (o, t, d) -> writeByteArray(o, t), ByteArrayTag.class);
+ put(StringTag.ID, (o, t, d) -> writeString(o, t), StringTag.class);
+ put(ListTag.ID, LittleEndianNBTOutputStream::writeList, ListTag.class);
+ put(CompoundTag.ID, LittleEndianNBTOutputStream::writeCompound, CompoundTag.class);
+ put(IntArrayTag.ID, (o, t, d) -> writeIntArray(o, t), IntArrayTag.class);
+ put(LongArrayTag.ID, (o, t, d) -> writeLongArray(o, t), LongArrayTag.class);
+ }
+
+ private static void put(byte id, ExceptionTriConsumer, Integer, IOException> f, Class> clazz) {
+ writers.put(id, f);
+ classIdMapping.put(clazz, id);
+ }
+
+ public LittleEndianNBTOutputStream(OutputStream out) {
+ output = new DataOutputStream(out);
+ }
+
+ public LittleEndianNBTOutputStream(DataOutputStream out) {
+ output = out;
+ }
+
+ public void writeTag(NamedTag tag, int maxDepth) throws IOException {
+ writeByte(tag.getTag().getID());
+ if (tag.getTag().getID() != 0) {
+ writeUTF(tag.getName() == null ? "" : tag.getName());
+ }
+ writeRawTag(tag.getTag(), maxDepth);
+ }
+
+ public void writeTag(Tag> tag, int maxDepth) throws IOException {
+ writeByte(tag.getID());
+ if (tag.getID() != 0) {
+ writeUTF("");
+ }
+ writeRawTag(tag, maxDepth);
+ }
+
+ public void writeRawTag(Tag> tag, int maxDepth) throws IOException {
+ ExceptionTriConsumer, Integer, IOException> f;
+ if ((f = writers.get(tag.getID())) == null) {
+ throw new IOException("invalid tag \"" + tag.getID() + "\"");
+ }
+ f.accept(this, tag, maxDepth);
+ }
+
+ static byte idFromClass(Class> clazz) {
+ Byte id = classIdMapping.get(clazz);
+ if (id == null) {
+ throw new IllegalArgumentException("unknown Tag class " + clazz.getName());
+ }
+ return id;
+ }
+
+ private static void writeByte(LittleEndianNBTOutputStream out, Tag> tag) throws IOException {
+ out.writeByte(((ByteTag) tag).asByte());
+ }
+
+ private static void writeShort(LittleEndianNBTOutputStream out, Tag> tag) throws IOException {
+ out.writeShort(((ShortTag) tag).asShort());
+ }
+
+ private static void writeInt(LittleEndianNBTOutputStream out, Tag> tag) throws IOException {
+ out.writeInt(((IntTag) tag).asInt());
+ }
+
+ private static void writeLong(LittleEndianNBTOutputStream out, Tag> tag) throws IOException {
+ out.writeLong(((LongTag) tag).asLong());
+ }
+
+ private static void writeFloat(LittleEndianNBTOutputStream out, Tag> tag) throws IOException {
+ out.writeFloat(((FloatTag) tag).asFloat());
+ }
+
+ private static void writeDouble(LittleEndianNBTOutputStream out, Tag> tag) throws IOException {
+ out.writeDouble(((DoubleTag) tag).asDouble());
+ }
+
+ private static void writeString(LittleEndianNBTOutputStream out, Tag> tag) throws IOException {
+ out.writeUTF(((StringTag) tag).getValue());
+ }
+
+ private static void writeByteArray(LittleEndianNBTOutputStream out, Tag> tag) throws IOException {
+ out.writeInt(((ByteArrayTag) tag).length());
+ out.write(((ByteArrayTag) tag).getValue());
+ }
+
+ private static void writeIntArray(LittleEndianNBTOutputStream out, Tag> tag) throws IOException {
+ out.writeInt(((IntArrayTag) tag).length());
+ for (int i : ((IntArrayTag) tag).getValue()) {
+ out.writeInt(i);
+ }
+ }
+
+ private static void writeLongArray(LittleEndianNBTOutputStream out, Tag> tag) throws IOException {
+ out.writeInt(((LongArrayTag) tag).length());
+ for (long l : ((LongArrayTag) tag).getValue()) {
+ out.writeLong(l);
+ }
+ }
+
+ private static void writeList(LittleEndianNBTOutputStream out, Tag> tag, int maxDepth) throws IOException {
+ out.writeByte(idFromClass(((ListTag>) tag).getTypeClass()));
+ out.writeInt(((ListTag>) tag).size());
+ for (Tag> t : ((ListTag>) tag)) {
+ out.writeRawTag(t, out.decrementMaxDepth(maxDepth));
+ }
+ }
+
+ private static void writeCompound(LittleEndianNBTOutputStream out, Tag> tag, int maxDepth) throws IOException {
+ for (Map.Entry> entry : (CompoundTag) tag) {
+ if (entry.getValue().getID() == 0) {
+ throw new IOException("end tag not allowed");
+ }
+ out.writeByte(entry.getValue().getID());
+ out.writeUTF(entry.getKey());
+ out.writeRawTag(entry.getValue(), out.decrementMaxDepth(maxDepth));
+ }
+ out.writeByte(0);
+ }
+
+ @Override
+ public void close() throws IOException {
+ output.close();
+ }
+
+ @Override
+ public void flush() throws IOException {
+ output.flush();
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ output.write(b);
+ }
+
+ @Override
+ public void write(byte[] b) throws IOException {
+ output.write(b);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ output.write(b, off, len);
+ }
+
+ @Override
+ public void writeBoolean(boolean v) throws IOException {
+ output.writeBoolean(v);
+ }
+
+ @Override
+ public void writeByte(int v) throws IOException {
+ output.writeByte(v);
+ }
+
+ @Override
+ public void writeShort(int v) throws IOException {
+ output.writeShort(Short.reverseBytes((short) v));
+ }
+
+ @Override
+ public void writeChar(int v) throws IOException {
+ output.writeChar(Character.reverseBytes((char) v));
+ }
+
+ @Override
+ public void writeInt(int v) throws IOException {
+ output.writeInt(Integer.reverseBytes(v));
+ }
+
+ @Override
+ public void writeLong(long v) throws IOException {
+ output.writeLong(Long.reverseBytes(v));
+ }
+
+ @Override
+ public void writeFloat(float v) throws IOException {
+ output.writeInt(Integer.reverseBytes(Float.floatToIntBits(v)));
+ }
+
+ @Override
+ public void writeDouble(double v) throws IOException {
+ output.writeLong(Long.reverseBytes(Double.doubleToLongBits(v)));
+ }
+
+ @Override
+ public void writeBytes(String s) throws IOException {
+ output.writeBytes(s);
+ }
+
+ @Override
+ public void writeChars(String s) throws IOException {
+ output.writeChars(s);
+ }
+
+ @Override
+ public void writeUTF(String s) throws IOException {
+ byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
+ writeShort(bytes.length);
+ write(bytes);
+ }
+}
diff --git a/src/main/java/net/querz/nbt/io/NBTDeserializer.java b/src/main/java/net/querz/nbt/io/NBTDeserializer.java
index 2f7289e3..085e37be 100644
--- a/src/main/java/net/querz/nbt/io/NBTDeserializer.java
+++ b/src/main/java/net/querz/nbt/io/NBTDeserializer.java
@@ -8,7 +8,7 @@
public class NBTDeserializer implements Deserializer {
- private boolean compressed;
+ private boolean compressed, littleEndian;
public NBTDeserializer() {
this(true);
@@ -18,13 +18,25 @@ public NBTDeserializer(boolean compressed) {
this.compressed = compressed;
}
+ public NBTDeserializer(boolean compressed, boolean littleEndian) {
+ this.compressed = compressed;
+ this.littleEndian = littleEndian;
+ }
+
@Override
public NamedTag fromStream(InputStream stream) throws IOException {
- NBTInputStream nbtIn;
+ NBTInput nbtIn;
+ InputStream input;
if (compressed) {
- nbtIn = new NBTInputStream(new GZIPInputStream(stream));
+ input = new GZIPInputStream(stream);
+ } else {
+ input = stream;
+ }
+
+ if (littleEndian) {
+ nbtIn = new LittleEndianNBTInputStream(input);
} else {
- nbtIn = new NBTInputStream(stream);
+ nbtIn = new NBTInputStream(input);
}
return nbtIn.readTag(Tag.DEFAULT_MAX_DEPTH);
}
diff --git a/src/main/java/net/querz/nbt/io/NBTInput.java b/src/main/java/net/querz/nbt/io/NBTInput.java
new file mode 100644
index 00000000..b17c6123
--- /dev/null
+++ b/src/main/java/net/querz/nbt/io/NBTInput.java
@@ -0,0 +1,11 @@
+package net.querz.nbt.io;
+
+import net.querz.nbt.tag.Tag;
+import java.io.IOException;
+
+public interface NBTInput {
+
+ NamedTag readTag(int maxDepth) throws IOException;
+
+ Tag> readRawTag(int maxDepth) throws IOException;
+}
diff --git a/src/main/java/net/querz/nbt/io/NBTInputStream.java b/src/main/java/net/querz/nbt/io/NBTInputStream.java
index b3ca6b8a..dd69c3c7 100644
--- a/src/main/java/net/querz/nbt/io/NBTInputStream.java
+++ b/src/main/java/net/querz/nbt/io/NBTInputStream.java
@@ -22,7 +22,7 @@
import java.util.HashMap;
import java.util.Map;
-public class NBTInputStream extends DataInputStream implements MaxDepthIO {
+public class NBTInputStream extends DataInputStream implements NBTInput, MaxDepthIO {
private static Map, IOException>> readers = new HashMap<>();
private static Map> idClassMapping = new HashMap<>();
diff --git a/src/main/java/net/querz/nbt/io/NBTOutput.java b/src/main/java/net/querz/nbt/io/NBTOutput.java
new file mode 100644
index 00000000..39f6d688
--- /dev/null
+++ b/src/main/java/net/querz/nbt/io/NBTOutput.java
@@ -0,0 +1,13 @@
+package net.querz.nbt.io;
+
+import net.querz.nbt.tag.Tag;
+import java.io.IOException;
+
+public interface NBTOutput {
+
+ void writeTag(NamedTag tag, int maxDepth) throws IOException;
+
+ void writeTag(Tag> tag, int maxDepth) throws IOException;
+
+ void flush() throws IOException;
+}
diff --git a/src/main/java/net/querz/nbt/io/NBTOutputStream.java b/src/main/java/net/querz/nbt/io/NBTOutputStream.java
index 4879052e..ae1a16b0 100644
--- a/src/main/java/net/querz/nbt/io/NBTOutputStream.java
+++ b/src/main/java/net/querz/nbt/io/NBTOutputStream.java
@@ -22,7 +22,7 @@
import java.util.HashMap;
import java.util.Map;
-public class NBTOutputStream extends DataOutputStream implements MaxDepthIO {
+public class NBTOutputStream extends DataOutputStream implements NBTOutput, MaxDepthIO {
private static Map, Integer, IOException>> writers = new HashMap<>();
private static Map, Byte> classIdMapping = new HashMap<>();
diff --git a/src/main/java/net/querz/nbt/io/NBTSerializer.java b/src/main/java/net/querz/nbt/io/NBTSerializer.java
index fe505992..921d8f47 100644
--- a/src/main/java/net/querz/nbt/io/NBTSerializer.java
+++ b/src/main/java/net/querz/nbt/io/NBTSerializer.java
@@ -8,7 +8,7 @@
public class NBTSerializer implements Serializer {
- private boolean compressed;
+ private boolean compressed, littleEndian;
public NBTSerializer() {
this(true);
@@ -18,13 +18,25 @@ public NBTSerializer(boolean compressed) {
this.compressed = compressed;
}
+ public NBTSerializer(boolean compressed, boolean littleEndian) {
+ this.compressed = compressed;
+ this.littleEndian = littleEndian;
+ }
+
@Override
public void toStream(NamedTag object, OutputStream out) throws IOException {
- NBTOutputStream nbtOut;
+ NBTOutput nbtOut;
+ OutputStream output;
if (compressed) {
- nbtOut = new NBTOutputStream(new GZIPOutputStream(out, true));
+ output = new GZIPOutputStream(out, true);
+ } else {
+ output = out;
+ }
+
+ if (littleEndian) {
+ nbtOut = new LittleEndianNBTOutputStream(output);
} else {
- nbtOut = new NBTOutputStream(out);
+ nbtOut = new NBTOutputStream(output);
}
nbtOut.writeTag(object, Tag.DEFAULT_MAX_DEPTH);
nbtOut.flush();
diff --git a/src/main/java/net/querz/nbt/io/NBTUtil.java b/src/main/java/net/querz/nbt/io/NBTUtil.java
index d8efc150..edd0b869 100644
--- a/src/main/java/net/querz/nbt/io/NBTUtil.java
+++ b/src/main/java/net/querz/nbt/io/NBTUtil.java
@@ -47,6 +47,40 @@ public static void write(Tag> tag, String file) throws IOException {
write(new NamedTag(null, tag), new File(file), true);
}
+ public static void writeLE(NamedTag tag, File file, boolean compressed) throws IOException {
+ try (FileOutputStream fos = new FileOutputStream(file)) {
+ new NBTSerializer(compressed, true).toStream(tag, fos);
+ }
+ }
+
+ public static void writeLE(NamedTag tag, String file, boolean compressed) throws IOException {
+ writeLE(tag, new File(file), compressed);
+ }
+
+ public static void writeLE(NamedTag tag, File file) throws IOException {
+ writeLE(tag, file, true);
+ }
+
+ public static void writeLE(NamedTag tag, String file) throws IOException {
+ writeLE(tag, new File(file), true);
+ }
+
+ public static void writeLE(Tag> tag, File file, boolean compressed) throws IOException {
+ writeLE(new NamedTag(null, tag), file, compressed);
+ }
+
+ public static void writeLE(Tag> tag, String file, boolean compressed) throws IOException {
+ writeLE(new NamedTag(null, tag), new File(file), compressed);
+ }
+
+ public static void writeLE(Tag> tag, File file) throws IOException {
+ writeLE(new NamedTag(null, tag), file, true);
+ }
+
+ public static void writeLE(Tag> tag, String file) throws IOException {
+ writeLE(new NamedTag(null, tag), new File(file), true);
+ }
+
public static NamedTag read(File file, boolean compressed) throws IOException {
try (FileInputStream fis = new FileInputStream(file)) {
return new NBTDeserializer(compressed).fromStream(fis);
@@ -67,6 +101,26 @@ public static NamedTag read(String file) throws IOException {
return read(new File(file));
}
+ public static NamedTag readLE(File file, boolean compressed) throws IOException {
+ try (FileInputStream fis = new FileInputStream(file)) {
+ return new NBTDeserializer(compressed, true).fromStream(fis);
+ }
+ }
+
+ public static NamedTag readLE(String file, boolean compressed) throws IOException {
+ return readLE(new File(file), compressed);
+ }
+
+ public static NamedTag readLE(File file) throws IOException {
+ try (FileInputStream fis = new FileInputStream(file)) {
+ return new NBTDeserializer(false, true).fromStream(detectDecompression(fis));
+ }
+ }
+
+ public static NamedTag readLE(String file) throws IOException {
+ return readLE(new File(file));
+ }
+
private static InputStream detectDecompression(InputStream is) throws IOException {
PushbackInputStream pbis = new PushbackInputStream(is, 2);
int signature = (pbis.read() & 0xFF) + (pbis.read() << 8);
diff --git a/src/main/java/net/querz/nbt/tag/ListTag.java b/src/main/java/net/querz/nbt/tag/ListTag.java
index 008e3aaa..f26c4b07 100644
--- a/src/main/java/net/querz/nbt/tag/ListTag.java
+++ b/src/main/java/net/querz/nbt/tag/ListTag.java
@@ -12,8 +12,7 @@
/**
* ListTag represents a typed List in the nbt structure.
- * An empty {@link ListTag} created using {@link ListTag#createUnchecked(Class)} will be of unknown type
- * and returns an {@link EndTag}{@code .class} in {@link ListTag#getTypeClass()}.
+ * An empty {@link ListTag} will be of type {@link EndTag} (unknown type).
* The type of an empty untyped {@link ListTag} can be set by using any of the {@code add()}
* methods or any of the {@code as...List()} methods.
* */
@@ -123,7 +122,7 @@ public void add(T t) {
public void add(int index, T t) {
Objects.requireNonNull(t);
- if (typeClass == null || typeClass == EndTag.class) {
+ if (getTypeClass() == EndTag.class) {
typeClass = t.getClass();
} else if (typeClass != t.getClass()) {
throw new ClassCastException(
@@ -203,7 +202,6 @@ public int indexOf(T t) {
@SuppressWarnings("unchecked")
public > ListTag asTypedList(Class type) {
checkTypeClass(type);
- typeClass = type;
return (ListTag) this;
}
@@ -309,7 +307,7 @@ public ListTag clone() {
//TODO: make private
@SuppressWarnings("unchecked")
public void addUnchecked(Tag> tag) {
- if (typeClass != null && typeClass != tag.getClass() && typeClass != EndTag.class) {
+ if (getTypeClass() != EndTag.class && typeClass != tag.getClass()) {
throw new IllegalArgumentException(String.format(
"cannot add %s to ListTag<%s>",
tag.getClass().getSimpleName(), typeClass.getSimpleName()));
@@ -318,7 +316,7 @@ public void addUnchecked(Tag> tag) {
}
private void checkTypeClass(Class> clazz) {
- if (typeClass != null && typeClass != EndTag.class && typeClass != clazz) {
+ if (getTypeClass() != EndTag.class && typeClass != clazz) {
throw new ClassCastException(String.format(
"cannot cast ListTag<%s> to ListTag<%s>",
typeClass.getSimpleName(), clazz.getSimpleName()));
diff --git a/src/test/java/net/querz/mca/MCAFileTest.java b/src/test/java/net/querz/mca/MCAFileTest.java
index 38e09f49..2688001a 100644
--- a/src/test/java/net/querz/mca/MCAFileTest.java
+++ b/src/test/java/net/querz/mca/MCAFileTest.java
@@ -1,6 +1,7 @@
package net.querz.mca;
import net.querz.nbt.tag.CompoundTag;
+import net.querz.nbt.tag.EndTag;
import net.querz.nbt.tag.ListTag;
import static net.querz.mca.LoadFlags.*;
import java.io.File;
@@ -90,7 +91,7 @@ public void testGetters() {
assertNotNull(f.getChunk(0).getBiomes());
assertNull(f.getChunk(0).getHeightMaps());
assertNull(f.getChunk(0).getCarvingMasks());
- assertEquals(new ListTag<>(CompoundTag.class), f.getChunk(0).getEntities());
+ assertEquals(ListTag.createUnchecked(null), f.getChunk(0).getEntities());
assertNull(f.getChunk(0).getTileEntities());
assertNull(f.getChunk(0).getTileTicks());
assertNull(f.getChunk(0).getLiquidTicks());
@@ -184,7 +185,7 @@ public void testGetBiomeAt() {
MCAFile f = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("r.2.2.mca")));
assertEquals(21, f.getBiomeAt(1024, 1024));
assertEquals(-1, f.getBiomeAt(1040, 1024));
- f.setChunk(0, 1, Chunk.newChunk());
+ f.setChunk(0, 1, Chunk.newChunk(2201));
assertEquals(-1, f.getBiomeAt(1024, 1040));
}
@@ -197,9 +198,9 @@ public void testSetBiomeAt() {
assertEquals(47, f.getChunk(64, 64).updateHandle(64, 64).getCompoundTag("Level").getIntArray("Biomes")[255]);
f.setBiomeAt(1040, 1024, 20);
int[] biomes = f.getChunk(65, 64).updateHandle(65, 64).getCompoundTag("Level").getIntArray("Biomes");
- assertEquals(256, biomes.length);
- for (int i = 0; i < 256; i++) {
- assertTrue(i == 0 ? biomes[i] == 20 : biomes[i] == -1);
+ assertEquals(1024, biomes.length);
+ for (int i = 0; i < 1024; i++) {
+ assertTrue(i % 16 == 0 ? biomes[i] == 20 : biomes[i] == -1);
}
}
diff --git a/src/test/java/net/querz/nbt/tag/ListTagTest.java b/src/test/java/net/querz/nbt/tag/ListTagTest.java
index 777d165b..af2b72fb 100644
--- a/src/test/java/net/querz/nbt/tag/ListTagTest.java
+++ b/src/test/java/net/querz/nbt/tag/ListTagTest.java
@@ -72,11 +72,11 @@ public void testEquals() {
ListTag> lu2 = ListTag.createUnchecked(null);
assertTrue(lu.equals(lu2));
lu2.asIntTagList();
- assertFalse(lu.equals(lu2));
+ assertTrue(lu.equals(lu2));
ListTag lie = new ListTag<>(IntTag.class);
assertFalse(lu.equals(lie));
lu.asIntTagList();
- assertTrue(lie.equals(lu));
+ assertFalse(lie.equals(lu));
}
public void testHashCode() {
@@ -91,7 +91,8 @@ public void testClone() {
ListTag i = new ListTag<>(IntTag.class);
ListTag c = i.clone();
assertThrowsRuntimeException(() -> c.addString("wrong type in clone"), IllegalArgumentException.class);
- c.addInt(123);
+ assertThrowsNoRuntimeException(() -> c.addInt(123));
+
assertFalse(i.equals(c));
c.clear();
assertTrue(i.equals(c));
@@ -203,8 +204,8 @@ public void testCasting() {
ListTag> lg = ListTag.createUnchecked(null);
ListTag lb = assertThrowsNoRuntimeException(lg::asByteTagList);
assertEquals(lb, lg);
- //only allow casting once from untyped list to typed list
- assertThrowsRuntimeException(lg::asShortTagList, ClassCastException.class);
+ // allow casting to a different list type if the list is empty
+ assertThrowsNoException(lg::asShortTagList);
}
public void testCompareTo() {