From 4f3b6e4029096139aca6657a3dc14e3ce9d50034 Mon Sep 17 00:00:00 2001 From: Daniel Mills Date: Mon, 28 Dec 2020 06:10:41 -0500 Subject: [PATCH] Smoothiemaps & nbt --- src/main/java/net/querz/io/Deserializer.java | 42 ++ .../net/querz/io/ExceptionBiFunction.java | 7 + .../net/querz/io/ExceptionTriConsumer.java | 7 + src/main/java/net/querz/io/MaxDepthIO.java | 13 + .../querz/io/MaxDepthReachedException.java | 12 + src/main/java/net/querz/io/Serializer.java | 26 + .../java/net/querz/io/StringDeserializer.java | 37 + .../java/net/querz/io/StringSerializer.java | 35 + src/main/java/net/querz/mca/Chunk.java | 643 ++++++++++++++++++ .../java/net/querz/mca/CompressionType.java | 49 ++ .../java/net/querz/mca/ExceptionFunction.java | 7 + src/main/java/net/querz/mca/LoadFlags.java | 24 + src/main/java/net/querz/mca/MCAFile.java | 295 ++++++++ src/main/java/net/querz/mca/MCAUtil.java | 223 ++++++ src/main/java/net/querz/mca/Section.java | 398 +++++++++++ .../net/querz/nbt/io/NBTDeserializer.java | 31 + .../java/net/querz/nbt/io/NBTInputStream.java | 149 ++++ .../net/querz/nbt/io/NBTOutputStream.java | 153 +++++ .../java/net/querz/nbt/io/NBTSerializer.java | 32 + src/main/java/net/querz/nbt/io/NBTUtil.java | 80 +++ src/main/java/net/querz/nbt/io/NamedTag.java | 30 + .../java/net/querz/nbt/io/ParseException.java | 25 + .../net/querz/nbt/io/SNBTDeserializer.java | 26 + .../java/net/querz/nbt/io/SNBTParser.java | 244 +++++++ .../java/net/querz/nbt/io/SNBTSerializer.java | 18 + src/main/java/net/querz/nbt/io/SNBTUtil.java | 15 + .../java/net/querz/nbt/io/SNBTWriter.java | 129 ++++ .../java/net/querz/nbt/io/StringPointer.java | 114 ++++ src/main/java/net/querz/nbt/tag/ArrayTag.java | 46 ++ .../java/net/querz/nbt/tag/ByteArrayTag.java | 42 ++ src/main/java/net/querz/nbt/tag/ByteTag.java | 47 ++ .../java/net/querz/nbt/tag/CompoundTag.java | 278 ++++++++ .../java/net/querz/nbt/tag/DoubleTag.java | 39 ++ src/main/java/net/querz/nbt/tag/EndTag.java | 31 + src/main/java/net/querz/nbt/tag/FloatTag.java | 39 ++ .../java/net/querz/nbt/tag/IntArrayTag.java | 42 ++ src/main/java/net/querz/nbt/tag/IntTag.java | 39 ++ src/main/java/net/querz/nbt/tag/ListTag.java | 327 +++++++++ .../java/net/querz/nbt/tag/LongArrayTag.java | 42 ++ src/main/java/net/querz/nbt/tag/LongTag.java | 39 ++ .../net/querz/nbt/tag/NonNullEntrySet.java | 140 ++++ .../java/net/querz/nbt/tag/NumberTag.java | 37 + src/main/java/net/querz/nbt/tag/ShortTag.java | 39 ++ .../java/net/querz/nbt/tag/StringTag.java | 50 ++ src/main/java/net/querz/nbt/tag/Tag.java | 187 +++++ 45 files changed, 4328 insertions(+) create mode 100644 src/main/java/net/querz/io/Deserializer.java create mode 100644 src/main/java/net/querz/io/ExceptionBiFunction.java create mode 100644 src/main/java/net/querz/io/ExceptionTriConsumer.java create mode 100644 src/main/java/net/querz/io/MaxDepthIO.java create mode 100644 src/main/java/net/querz/io/MaxDepthReachedException.java create mode 100644 src/main/java/net/querz/io/Serializer.java create mode 100644 src/main/java/net/querz/io/StringDeserializer.java create mode 100644 src/main/java/net/querz/io/StringSerializer.java create mode 100644 src/main/java/net/querz/mca/Chunk.java create mode 100644 src/main/java/net/querz/mca/CompressionType.java create mode 100644 src/main/java/net/querz/mca/ExceptionFunction.java create mode 100644 src/main/java/net/querz/mca/LoadFlags.java create mode 100644 src/main/java/net/querz/mca/MCAFile.java create mode 100644 src/main/java/net/querz/mca/MCAUtil.java create mode 100644 src/main/java/net/querz/mca/Section.java create mode 100644 src/main/java/net/querz/nbt/io/NBTDeserializer.java create mode 100644 src/main/java/net/querz/nbt/io/NBTInputStream.java create mode 100644 src/main/java/net/querz/nbt/io/NBTOutputStream.java create mode 100644 src/main/java/net/querz/nbt/io/NBTSerializer.java create mode 100644 src/main/java/net/querz/nbt/io/NBTUtil.java create mode 100644 src/main/java/net/querz/nbt/io/NamedTag.java create mode 100644 src/main/java/net/querz/nbt/io/ParseException.java create mode 100644 src/main/java/net/querz/nbt/io/SNBTDeserializer.java create mode 100644 src/main/java/net/querz/nbt/io/SNBTParser.java create mode 100644 src/main/java/net/querz/nbt/io/SNBTSerializer.java create mode 100644 src/main/java/net/querz/nbt/io/SNBTUtil.java create mode 100644 src/main/java/net/querz/nbt/io/SNBTWriter.java create mode 100644 src/main/java/net/querz/nbt/io/StringPointer.java create mode 100644 src/main/java/net/querz/nbt/tag/ArrayTag.java create mode 100644 src/main/java/net/querz/nbt/tag/ByteArrayTag.java create mode 100644 src/main/java/net/querz/nbt/tag/ByteTag.java create mode 100644 src/main/java/net/querz/nbt/tag/CompoundTag.java create mode 100644 src/main/java/net/querz/nbt/tag/DoubleTag.java create mode 100644 src/main/java/net/querz/nbt/tag/EndTag.java create mode 100644 src/main/java/net/querz/nbt/tag/FloatTag.java create mode 100644 src/main/java/net/querz/nbt/tag/IntArrayTag.java create mode 100644 src/main/java/net/querz/nbt/tag/IntTag.java create mode 100644 src/main/java/net/querz/nbt/tag/ListTag.java create mode 100644 src/main/java/net/querz/nbt/tag/LongArrayTag.java create mode 100644 src/main/java/net/querz/nbt/tag/LongTag.java create mode 100644 src/main/java/net/querz/nbt/tag/NonNullEntrySet.java create mode 100644 src/main/java/net/querz/nbt/tag/NumberTag.java create mode 100644 src/main/java/net/querz/nbt/tag/ShortTag.java create mode 100644 src/main/java/net/querz/nbt/tag/StringTag.java create mode 100644 src/main/java/net/querz/nbt/tag/Tag.java diff --git a/src/main/java/net/querz/io/Deserializer.java b/src/main/java/net/querz/io/Deserializer.java new file mode 100644 index 000000000..1849fe9dd --- /dev/null +++ b/src/main/java/net/querz/io/Deserializer.java @@ -0,0 +1,42 @@ +package net.querz.io; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +public interface Deserializer { + + T fromStream(InputStream stream) throws IOException; + + default T fromFile(File file) throws IOException { + try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) { + return fromStream(bis); + } + } + + default T fromBytes(byte[] data) throws IOException { + ByteArrayInputStream stream = new ByteArrayInputStream(data); + return fromStream(stream); + } + + default T fromResource(Class clazz, String path) throws IOException { + try (InputStream stream = clazz.getClassLoader().getResourceAsStream(path)) { + if (stream == null) { + throw new IOException("resource \"" + path + "\" not found"); + } + return fromStream(stream); + } + } + + default T fromURL(URL url) throws IOException { + try (InputStream stream = url.openStream()) { + return fromStream(stream); + } + } + + +} diff --git a/src/main/java/net/querz/io/ExceptionBiFunction.java b/src/main/java/net/querz/io/ExceptionBiFunction.java new file mode 100644 index 000000000..c34dba722 --- /dev/null +++ b/src/main/java/net/querz/io/ExceptionBiFunction.java @@ -0,0 +1,7 @@ +package net.querz.io; + +@FunctionalInterface +public interface ExceptionBiFunction { + + R accept(T t, U u) throws E; +} diff --git a/src/main/java/net/querz/io/ExceptionTriConsumer.java b/src/main/java/net/querz/io/ExceptionTriConsumer.java new file mode 100644 index 000000000..d49ccc908 --- /dev/null +++ b/src/main/java/net/querz/io/ExceptionTriConsumer.java @@ -0,0 +1,7 @@ +package net.querz.io; + +@FunctionalInterface +public interface ExceptionTriConsumer { + + void accept(T t, U u, V v) throws E; +} diff --git a/src/main/java/net/querz/io/MaxDepthIO.java b/src/main/java/net/querz/io/MaxDepthIO.java new file mode 100644 index 000000000..0a5fc3e70 --- /dev/null +++ b/src/main/java/net/querz/io/MaxDepthIO.java @@ -0,0 +1,13 @@ +package net.querz.io; + +public interface MaxDepthIO { + + default int decrementMaxDepth(int maxDepth) { + if (maxDepth < 0) { + throw new IllegalArgumentException("negative maximum depth is not allowed"); + } else if (maxDepth == 0) { + throw new MaxDepthReachedException("reached maximum depth of NBT structure"); + } + return --maxDepth; + } +} diff --git a/src/main/java/net/querz/io/MaxDepthReachedException.java b/src/main/java/net/querz/io/MaxDepthReachedException.java new file mode 100644 index 000000000..eb9032289 --- /dev/null +++ b/src/main/java/net/querz/io/MaxDepthReachedException.java @@ -0,0 +1,12 @@ +package net.querz.io; + +/** + * Exception indicating that the maximum (de-)serialization depth has been reached. + */ +@SuppressWarnings("serial") +public class MaxDepthReachedException extends RuntimeException { + + public MaxDepthReachedException(String msg) { + super(msg); + } +} diff --git a/src/main/java/net/querz/io/Serializer.java b/src/main/java/net/querz/io/Serializer.java new file mode 100644 index 000000000..a6c9377a9 --- /dev/null +++ b/src/main/java/net/querz/io/Serializer.java @@ -0,0 +1,26 @@ +package net.querz.io; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +public interface Serializer { + + void toStream(T object, OutputStream out) throws IOException; + + default void toFile(T object, File file) throws IOException { + try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file))) { + toStream(object, bos); + } + } + + default byte[] toBytes(T object) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + toStream(object, bos); + bos.close(); + return bos.toByteArray(); + } +} diff --git a/src/main/java/net/querz/io/StringDeserializer.java b/src/main/java/net/querz/io/StringDeserializer.java new file mode 100644 index 000000000..2160e2a8b --- /dev/null +++ b/src/main/java/net/querz/io/StringDeserializer.java @@ -0,0 +1,37 @@ +package net.querz.io; + +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; + +public interface StringDeserializer extends Deserializer { + + T fromReader(Reader reader) throws IOException; + + default T fromString(String s) throws IOException { + return fromReader(new StringReader(s)); + } + + @Override + default T fromStream(InputStream stream) throws IOException { + try (Reader reader = new InputStreamReader(stream)) { + return fromReader(reader); + } + } + + @Override + default T fromFile(File file) throws IOException { + try (Reader reader = new FileReader(file)) { + return fromReader(reader); + } + } + + @Override + default T fromBytes(byte[] data) throws IOException { + return fromReader(new StringReader(new String(data))); + } +} diff --git a/src/main/java/net/querz/io/StringSerializer.java b/src/main/java/net/querz/io/StringSerializer.java new file mode 100644 index 000000000..c4da81042 --- /dev/null +++ b/src/main/java/net/querz/io/StringSerializer.java @@ -0,0 +1,35 @@ +package net.querz.io; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.StringWriter; +import java.io.Writer; + +public interface StringSerializer extends Serializer { + + void toWriter(T object, Writer writer) throws IOException; + + default String toString(T object) throws IOException { + Writer writer = new StringWriter(); + toWriter(object, writer); + writer.flush(); + return writer.toString(); + } + + @Override + default void toStream(T object, OutputStream stream) throws IOException { + Writer writer = new OutputStreamWriter(stream); + toWriter(object, writer); + writer.flush(); + } + + @Override + default void toFile(T object, File file) throws IOException { + try (Writer writer = new FileWriter(file)) { + toWriter(object, writer); + } + } +} diff --git a/src/main/java/net/querz/mca/Chunk.java b/src/main/java/net/querz/mca/Chunk.java new file mode 100644 index 000000000..d305df04d --- /dev/null +++ b/src/main/java/net/querz/mca/Chunk.java @@ -0,0 +1,643 @@ +package net.querz.mca; + +import net.querz.nbt.tag.CompoundTag; +import net.querz.nbt.tag.ListTag; +import net.querz.nbt.io.NamedTag; +import net.querz.nbt.io.NBTDeserializer; +import net.querz.nbt.io.NBTSerializer; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.util.Arrays; +import static net.querz.mca.LoadFlags.*; + +public class Chunk { + + public static final int DEFAULT_DATA_VERSION = 1628; + + private boolean partial; + + private int lastMCAUpdate; + + private CompoundTag data; + + private int dataVersion; + private long lastUpdate; + private long inhabitedTime; + private int[] biomes; + private CompoundTag heightMaps; + private CompoundTag carvingMasks; + private Section[] sections = new Section[16]; //always initialized with size = 16 for fast access + private ListTag entities; + private ListTag tileEntities; + private ListTag tileTicks; + private ListTag liquidTicks; + private ListTag> lights; + private ListTag> liquidsToBeTicked; + private ListTag> toBeTicked; + private ListTag> postProcessing; + private String status; + private CompoundTag structures; + + Chunk(int lastMCAUpdate) { + this.lastMCAUpdate = lastMCAUpdate; + } + + /** + * Create a new chunk based on raw base data from a region file. + * @param data The raw base data to be used. + */ + public Chunk(CompoundTag data) { + this.data = data; + initReferences(ALL_DATA); + } + + private void initReferences(long loadFlags) { + if (data == null) { + throw new NullPointerException("data cannot be null"); + } + CompoundTag level; + if ((level = data.getCompoundTag("Level")) == null) { + throw new IllegalArgumentException("data does not contain \"Level\" tag"); + } + dataVersion = data.getInt("DataVersion"); + inhabitedTime = level.getLong("InhabitedTime"); + lastUpdate = level.getLong("LastUpdate"); + if ((loadFlags & BIOMES) != 0) { + biomes = level.getIntArray("Biomes"); + } + if ((loadFlags & HEIGHTMAPS) != 0) { + heightMaps = level.getCompoundTag("Heightmaps"); + } + if ((loadFlags & CARVING_MASKS) != 0) { + carvingMasks = level.getCompoundTag("CarvingMasks"); + } + if ((loadFlags & ENTITIES) != 0) { + entities = level.containsKey("Entities") ? level.getListTag("Entities").asCompoundTagList() : null; + } + if ((loadFlags & TILE_ENTITIES) != 0) { + tileEntities = level.containsKey("TileEntities") ? level.getListTag("TileEntities").asCompoundTagList() : null; + } + if ((loadFlags & TILE_TICKS) != 0) { + tileTicks = level.containsKey("TileTicks") ? level.getListTag("TileTicks").asCompoundTagList() : null; + } + if ((loadFlags & LIQUID_TICKS) != 0) { + liquidTicks = level.containsKey("LiquidTicks") ? level.getListTag("LiquidTicks").asCompoundTagList() : null; + } + if ((loadFlags & LIGHTS) != 0) { + lights = level.containsKey("Lights") ? level.getListTag("Lights").asListTagList() : null; + } + if ((loadFlags & LIQUIDS_TO_BE_TICKED) != 0) { + liquidsToBeTicked = level.containsKey("LiquidsToBeTicked") ? level.getListTag("LiquidsToBeTicked").asListTagList() : null; + } + if ((loadFlags & TO_BE_TICKED) != 0) { + toBeTicked = level.containsKey("ToBeTicked") ? level.getListTag("ToBeTicked").asListTagList() : null; + } + if ((loadFlags & POST_PROCESSING) != 0) { + postProcessing = level.containsKey("PostProcessing") ? level.getListTag("PostProcessing").asListTagList() : null; + } + status = level.getString("Status"); + if ((loadFlags & STRUCTURES) != 0) { + structures = level.getCompoundTag("Structures"); + } + if ((loadFlags & (BLOCK_LIGHTS|BLOCK_STATES|SKY_LIGHT)) != 0 && level.containsKey("Sections")) { + for (CompoundTag section : level.getListTag("Sections").asCompoundTagList()) { + int sectionIndex = section.getByte("Y"); + if (sectionIndex > 15 || sectionIndex < 0) { + continue; + } + Section newSection = new Section(section, dataVersion, loadFlags); + if (newSection.isEmpty()) { + continue; + } + sections[sectionIndex] = newSection; + } + } + + // If we haven't requested the full set of data we can drop the underlying raw data to let the GC handle it. + if (loadFlags != ALL_DATA) { + data = null; + partial = true; + } else { + partial = false; + } + } + + /** + * Serializes this chunk to a RandomAccessFile. + * @param raf The RandomAccessFile to be written to. + * @param xPos The x-coordinate of the chunk. + * @param zPos The z-coodrinate of the chunk. + * @return The amount of bytes written to the RandomAccessFile. + * @throws UnsupportedOperationException When something went wrong during writing. + * @throws IOException When something went wrong during writing. + */ + public int serialize(RandomAccessFile raf, int xPos, int zPos) throws IOException { + if (partial) { + throw new UnsupportedOperationException("Partially loaded chunks cannot be serialized"); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(4096); + try (BufferedOutputStream nbtOut = new BufferedOutputStream(CompressionType.ZLIB.compress(baos))) { + new NBTSerializer(false).toStream(new NamedTag(null, updateHandle(xPos, zPos)), nbtOut); + } + byte[] rawData = baos.toByteArray(); + raf.writeInt(rawData.length + 1); // including the byte to store the compression type + raf.writeByte(CompressionType.ZLIB.getID()); + raf.write(rawData); + return rawData.length + 5; + } + + /** + * Reads chunk data from a RandomAccessFile. The RandomAccessFile must already be at the correct position. + * @param raf The RandomAccessFile to read the chunk data from. + * @throws IOException When something went wrong during reading. + */ + public void deserialize(RandomAccessFile raf) throws IOException { + deserialize(raf, ALL_DATA); + } + + /** + * Reads chunk data from a RandomAccessFile. The RandomAccessFile must already be at the correct position. + * @param raf The RandomAccessFile to read the chunk data from. + * @param loadFlags A logical or of {@link LoadFlags} constants indicating what data should be loaded + * @throws IOException When something went wrong during reading. + */ + public void deserialize(RandomAccessFile raf, long loadFlags) throws IOException { + byte compressionTypeByte = raf.readByte(); + CompressionType compressionType = CompressionType.getFromID(compressionTypeByte); + if (compressionType == null) { + throw new IOException("invalid compression type " + compressionTypeByte); + } + BufferedInputStream dis = new BufferedInputStream(compressionType.decompress(new FileInputStream(raf.getFD()))); + NamedTag tag = new NBTDeserializer(false).fromStream(dis); + if (tag != null && tag.getTag() instanceof CompoundTag) { + data = (CompoundTag) tag.getTag(); + initReferences(loadFlags); + } else { + throw new IOException("invalid data tag: " + (tag == null ? "null" : tag.getClass().getName())); + } + } + + /** + * @deprecated Use {@link #getBiomeAt(int, int, int)} instead + */ + @Deprecated + public int getBiomeAt(int blockX, int blockZ) { + if (dataVersion < 2202) { + if (biomes == null || biomes.length != 256) { + return -1; + } + return biomes[getBlockIndex(blockX, blockZ)]; + } else { + throw new IllegalStateException("cannot get biome using Chunk#getBiomeAt(int,int) from biome data with DataVersion of 2202 or higher, use Chunk#getBiomeAt(int,int,int) instead"); + } + } + + /** + * Fetches a biome id at a specific block in this chunk. + * The coordinates can be absolute coordinates or relative to the region or chunk. + * @param blockX The x-coordinate of the block. + * @param blockY The y-coordinate of the block. + * @param blockZ The z-coordinate of the block. + * @return The biome id or -1 if the biomes are not correctly initialized. + */ + public int getBiomeAt(int blockX, int blockY, int blockZ) { + if (dataVersion < 2202) { + if (biomes == null || biomes.length != 256) { + return -1; + } + return biomes[getBlockIndex(blockX, blockZ)]; + } else { + if (biomes == null || biomes.length != 1024) { + return -1; + } + int biomeX = (blockX & 0xF) >> 2; + int biomeY = (blockY & 0xF) >> 2; + int biomeZ = (blockZ & 0xF) >> 2; + + return biomes[getBiomeIndex(biomeX, biomeY, biomeZ)]; + } + } + + @Deprecated + public void setBiomeAt(int blockX, int blockZ, int biomeID) { + if (dataVersion < 2202) { + if (biomes == null || biomes.length != 256) { + biomes = new int[256]; + Arrays.fill(biomes, -1); + } + biomes[getBlockIndex(blockX, blockZ)] = biomeID; + } else { + if (biomes == null || biomes.length != 1024) { + biomes = new int[1024]; + Arrays.fill(biomes, -1); + } + + int biomeX = (blockX & 0xF) >> 2; + int biomeZ = (blockZ & 0xF) >> 2; + + for (int y = 0; y < 64; y++) { + biomes[getBiomeIndex(biomeX, y, biomeZ)] = biomeID; + } + } + } + + /** + * Sets a biome id at a specific block column. + * The coordinates can be absolute coordinates or relative to the region or chunk. + * @param blockX The x-coordinate of the block column. + * @param blockZ The z-coordinate of the block column. + * @param biomeID The biome id to be set. + * 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) { + if (dataVersion < 2202) { + if (biomes == null || biomes.length != 256) { + biomes = new int[256]; + Arrays.fill(biomes, -1); + } + biomes[getBlockIndex(blockX, blockZ)] = biomeID; + } else { + if (biomes == null || biomes.length != 1024) { + biomes = new int[1024]; + Arrays.fill(biomes, -1); + } + + int biomeX = (blockX & 0xF) >> 2; + int biomeZ = (blockZ & 0xF) >> 2; + + biomes[getBiomeIndex(biomeX, blockY, biomeZ)] = biomeID; + } + } + + int getBiomeIndex(int biomeX, int biomeY, int biomeZ) { + return biomeY * 64 + biomeZ * 4 + biomeX; + } + + public CompoundTag getBlockStateAt(int blockX, int blockY, int blockZ) { + Section section = sections[MCAUtil.blockToChunk(blockY)]; + if (section == null) { + return null; + } + return section.getBlockStateAt(blockX, blockY, blockZ); + } + + /** + * Sets a block state at a specific location. + * The block coordinates can be absolute or relative to the region or chunk. + * @param blockX The x-coordinate of the block. + * @param blockY The y-coordinate of the block. + * @param blockZ The z-coordinate of the block. + * @param state The block state to be set. + * @param cleanup When true, it will cleanup all palettes of this chunk. + * This option should only be used moderately to avoid unnecessary recalculation of the palette indices. + * 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) { + int sectionIndex = MCAUtil.blockToChunk(blockY); + Section section = sections[sectionIndex]; + if (section == null) { + section = sections[sectionIndex] = Section.newSection(); + } + section.setBlockStateAt(blockX, blockY, blockZ, state, cleanup); + } + + /** + * @return The DataVersion of this chunk. + */ + public int getDataVersion() { + return dataVersion; + } + + /** + * Sets the DataVersion of this chunk. This does not check if the data of this chunk conforms + * to that DataVersion, that is the responsibility of the developer. + * @param dataVersion The DataVersion to be set. + */ + public void setDataVersion(int dataVersion) { + this.dataVersion = dataVersion; + } + + /** + * @return The timestamp when this region file was last updated in seconds since 1970-01-01. + */ + public int getLastMCAUpdate() { + return lastMCAUpdate; + } + + /** + * Sets the timestamp when this region file was last updated in seconds since 1970-01-01. + * @param lastMCAUpdate The time in seconds since 1970-01-01. + */ + public void setLastMCAUpdate(int lastMCAUpdate) { + this.lastMCAUpdate = lastMCAUpdate; + } + + /** + * @return The generation station of this chunk. + */ + public String getStatus() { + return status; + } + + /** + * Sets the generation status of this chunk. + * @param status The generation status of this chunk. + */ + public void setStatus(String status) { + this.status = status; + } + + /** + * Fetches the section at the given y-coordinate. + * @param sectionY The y-coordinate of the section in this chunk ranging from 0 to 15. + * @return The Section. + */ + public Section getSection(int sectionY) { + return sections[sectionY]; + } + + /** + * Sets a section at a givesn y-coordinate + * @param sectionY The y-coordinate of the section in this chunk ranging from 0 to 15. + * @param section The section to be set. + */ + public void setSection(int sectionY, Section section) { + sections[sectionY] = section; + } + + /** + * @return The timestamp when this chunk was last updated as a UNIX timestamp. + */ + public long getLastUpdate() { + return lastUpdate; + } + + /** + * Sets the time when this chunk was last updated as a UNIX timestamp. + * @param lastUpdate The UNIX timestamp. + */ + public void setLastUpdate(long lastUpdate) { + this.lastUpdate = lastUpdate; + } + + /** + * @return The cumulative amount of time players have spent in this chunk in ticks. + */ + public long getInhabitedTime() { + return inhabitedTime; + } + + /** + * Sets the cumulative amount of time players have spent in this chunk in ticks. + * @param inhabitedTime The time in ticks. + */ + public void setInhabitedTime(long inhabitedTime) { + this.inhabitedTime = inhabitedTime; + } + + /** + * @return A matrix of biome IDs for all block columns in this chunk. + */ + public int[] getBiomes() { + return biomes; + } + + /** + * Sets the biome IDs for this chunk. + * @param biomes The biome ID matrix of this chunk. Must have a length of 256. + * @throws IllegalArgumentException When the biome matrix does not have a length of 256 + * or is null + */ + public void setBiomes(int[] biomes) { + 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")); + } + } + this.biomes = biomes; + } + + /** + * @return The height maps of this chunk. + */ + public CompoundTag getHeightMaps() { + return heightMaps; + } + + /** + * Sets the height maps of this chunk. + * @param heightMaps The height maps. + */ + public void setHeightMaps(CompoundTag heightMaps) { + this.heightMaps = heightMaps; + } + + /** + * @return The carving masks of this chunk. + */ + public CompoundTag getCarvingMasks() { + return carvingMasks; + } + + /** + * Sets the carving masks of this chunk. + * @param carvingMasks The carving masks. + */ + public void setCarvingMasks(CompoundTag carvingMasks) { + this.carvingMasks = carvingMasks; + } + + /** + * @return The entities of this chunk. + */ + public ListTag getEntities() { + return entities; + } + + /** + * Sets the entities of this chunk. + * @param entities The entities. + */ + public void setEntities(ListTag entities) { + this.entities = entities; + } + + /** + * @return The tile entities of this chunk. + */ + public ListTag getTileEntities() { + return tileEntities; + } + + /** + * Sets the tile entities of this chunk. + * @param tileEntities The tile entities of this chunk. + */ + public void setTileEntities(ListTag tileEntities) { + this.tileEntities = tileEntities; + } + + /** + * @return The tile ticks of this chunk. + */ + public ListTag getTileTicks() { + return tileTicks; + } + + /** + * Sets the tile ticks of this chunk. + * @param tileTicks Thee tile ticks. + */ + public void setTileTicks(ListTag tileTicks) { + this.tileTicks = tileTicks; + } + + /** + * @return The liquid ticks of this chunk. + */ + public ListTag getLiquidTicks() { + return liquidTicks; + } + + /** + * Sets the liquid ticks of this chunk. + * @param liquidTicks The liquid ticks. + */ + public void setLiquidTicks(ListTag liquidTicks) { + this.liquidTicks = liquidTicks; + } + + /** + * @return The light sources in this chunk. + */ + public ListTag> getLights() { + return lights; + } + + /** + * Sets the light sources in this chunk. + * @param lights The light sources. + */ + public void setLights(ListTag> lights) { + this.lights = lights; + } + + /** + * @return THe liquids to be ticked in this chunk. + */ + public ListTag> getLiquidsToBeTicked() { + return liquidsToBeTicked; + } + + /** + * Sets the liquids to be ticked in this chunk. + * @param liquidsToBeTicked The liquids to be ticked. + */ + public void setLiquidsToBeTicked(ListTag> liquidsToBeTicked) { + this.liquidsToBeTicked = liquidsToBeTicked; + } + + /** + * @return Stuff to be ticked in this chunk. + */ + public ListTag> getToBeTicked() { + return toBeTicked; + } + + /** + * Sets stuff to be ticked in this chunk. + * @param toBeTicked The stuff to be ticked. + */ + public void setToBeTicked(ListTag> toBeTicked) { + this.toBeTicked = toBeTicked; + } + + /** + * @return Things that are in post processing in this chunk. + */ + public ListTag> getPostProcessing() { + return postProcessing; + } + + /** + * Sets things to be post processed in this chunk. + * @param postProcessing The things to be post processed. + */ + public void setPostProcessing(ListTag> postProcessing) { + this.postProcessing = postProcessing; + } + + /** + * @return Data about structures in this chunk. + */ + public CompoundTag getStructures() { + return structures; + } + + /** + * Sets data about structures in this chunk. + * @param structures The data about structures. + */ + public void setStructures(CompoundTag structures) { + this.structures = structures; + } + + int getBlockIndex(int blockX, int blockZ) { + return (blockZ & 0xF) * 16 + (blockX & 0xF); + } + + public void cleanupPalettesAndBlockStates() { + for (Section section : sections) { + if (section != null) { + section.cleanupPaletteAndBlockStates(); + } + } + } + + public static Chunk newChunk() { + Chunk c = new Chunk(0); + c.dataVersion = DEFAULT_DATA_VERSION; + c.data = new CompoundTag(); + c.data.put("Level", new CompoundTag()); + c.status = "mobs_spawned"; + return c; + } + + public CompoundTag updateHandle(int xPos, int zPos) { + data.putInt("DataVersion", dataVersion); + CompoundTag level = data.getCompoundTag("Level"); + level.putInt("xPos", xPos); + level.putInt("zPos", zPos); + level.putLong("LastUpdate", lastUpdate); + level.putLong("InhabitedTime", inhabitedTime); + if (dataVersion < 2202) { + 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); + level.putString("Status", status); + 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) { + sections.add(this.sections[i].updateHandle(i)); + } + } + level.put("Sections", sections); + return data; + } +} diff --git a/src/main/java/net/querz/mca/CompressionType.java b/src/main/java/net/querz/mca/CompressionType.java new file mode 100644 index 000000000..9ae41d4c6 --- /dev/null +++ b/src/main/java/net/querz/mca/CompressionType.java @@ -0,0 +1,49 @@ +package net.querz.mca; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; +import java.util.zip.InflaterInputStream; + +public enum CompressionType { + + NONE(0, t -> t, t -> t), + GZIP(1, GZIPOutputStream::new, GZIPInputStream::new), + ZLIB(2, DeflaterOutputStream::new, InflaterInputStream::new); + + private byte id; + private ExceptionFunction compressor; + private ExceptionFunction decompressor; + + CompressionType(int id, + ExceptionFunction compressor, + ExceptionFunction decompressor) { + this.id = (byte) id; + this.compressor = compressor; + this.decompressor = decompressor; + } + + public byte getID() { + return id; + } + + public OutputStream compress(OutputStream out) throws IOException { + return compressor.accept(out); + } + + public InputStream decompress(InputStream in) throws IOException { + return decompressor.accept(in); + } + + public static CompressionType getFromID(byte id) { + for (CompressionType c : CompressionType.values()) { + if (c.id == id) { + return c; + } + } + return null; + } +} diff --git a/src/main/java/net/querz/mca/ExceptionFunction.java b/src/main/java/net/querz/mca/ExceptionFunction.java new file mode 100644 index 000000000..40fe81959 --- /dev/null +++ b/src/main/java/net/querz/mca/ExceptionFunction.java @@ -0,0 +1,7 @@ +package net.querz.mca; + +@FunctionalInterface +public interface ExceptionFunction { + + R accept(T t) throws E; +} diff --git a/src/main/java/net/querz/mca/LoadFlags.java b/src/main/java/net/querz/mca/LoadFlags.java new file mode 100644 index 000000000..e41228983 --- /dev/null +++ b/src/main/java/net/querz/mca/LoadFlags.java @@ -0,0 +1,24 @@ +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; + + +} diff --git a/src/main/java/net/querz/mca/MCAFile.java b/src/main/java/net/querz/mca/MCAFile.java new file mode 100644 index 000000000..920372244 --- /dev/null +++ b/src/main/java/net/querz/mca/MCAFile.java @@ -0,0 +1,295 @@ +package net.querz.mca; + +import net.querz.nbt.tag.CompoundTag; +import java.io.IOException; +import java.io.RandomAccessFile; + +public class MCAFile { + + /** + * The default chunk data version used when no custom version is supplied. + * */ + public static final int DEFAULT_DATA_VERSION = 1628; + + private int regionX, regionZ; + private Chunk[] chunks; + + /** + * MCAFile represents a world save file used by Minecraft to store world + * data on the hard drive. + * This constructor needs the x- and z-coordinates of the stored region, + * which can usually be taken from the file name {@code r.x.z.mca} + * @param regionX The x-coordinate of this region. + * @param regionZ The z-coordinate of this region. + * */ + public MCAFile(int regionX, int regionZ) { + this.regionX = regionX; + this.regionZ = regionZ; + } + + /** + * Reads an .mca file from a {@code RandomAccessFile} into this object. + * This method does not perform any cleanups on the data. + * @param raf The {@code RandomAccessFile} to read from. + * @throws IOException If something went wrong during deserialization. + * */ + public void deserialize(RandomAccessFile raf) throws IOException { + deserialize(raf, LoadFlags.ALL_DATA); + } + + /** + * Reads an .mca file from a {@code RandomAccessFile} into this object. + * This method does not perform any cleanups on the data. + * @param raf The {@code RandomAccessFile} to read from. + * @param loadFlags A logical or of {@link LoadFlags} constants indicating what data should be loaded + * @throws IOException If something went wrong during deserialization. + * */ + public void deserialize(RandomAccessFile raf, long loadFlags) throws IOException { + chunks = new Chunk[1024]; + for (int i = 0; i < 1024; i++) { + raf.seek(i * 4); + int offset = raf.read() << 16; + offset |= (raf.read() & 0xFF) << 8; + offset |= raf.read() & 0xFF; + if (raf.readByte() == 0) { + continue; + } + raf.seek(4096 + i * 4); + int timestamp = raf.readInt(); + Chunk chunk = new Chunk(timestamp); + raf.seek(4096 * offset + 4); //+4: skip data size + chunk.deserialize(raf, loadFlags); + chunks[i] = chunk; + } + } + + /** + * Calls {@link MCAFile#serialize(RandomAccessFile, boolean)} without updating any timestamps. + * @see MCAFile#serialize(RandomAccessFile, boolean) + * @param raf The {@code RandomAccessFile} to write to. + * @return The amount of chunks written to the file. + * @throws IOException If something went wrong during serialization. + * */ + public int serialize(RandomAccessFile raf) throws IOException { + return serialize(raf, false); + } + + /** + * Serializes this object to an .mca file. + * This method does not perform any cleanups on the data. + * @param raf The {@code RandomAccessFile} to write to. + * @param changeLastUpdate Whether it should update all timestamps that show + * when this file was last updated. + * @return The amount of chunks written to the file. + * @throws IOException If something went wrong during serialization. + * */ + public int serialize(RandomAccessFile raf, boolean changeLastUpdate) throws IOException { + int globalOffset = 2; + int lastWritten = 0; + int timestamp = (int) (System.currentTimeMillis() / 1000L); + int chunksWritten = 0; + int chunkXOffset = MCAUtil.regionToChunk(regionX); + int chunkZOffset = MCAUtil.regionToChunk(regionZ); + + if (chunks == null) { + return 0; + } + + for (int cx = 0; cx < 32; cx++) { + for (int cz = 0; cz < 32; cz++) { + int index = getChunkIndex(cx, cz); + Chunk chunk = chunks[index]; + if (chunk == null) { + continue; + } + raf.seek(4096 * globalOffset); + lastWritten = chunk.serialize(raf, chunkXOffset + cx, chunkZOffset + cz); + + if (lastWritten == 0) { + continue; + } + + chunksWritten++; + + int sectors = (lastWritten >> 12) + (lastWritten % 4096 == 0 ? 0 : 1); + + raf.seek(index * 4); + raf.writeByte(globalOffset >>> 16); + raf.writeByte(globalOffset >> 8 & 0xFF); + raf.writeByte(globalOffset & 0xFF); + raf.writeByte(sectors); + + // write timestamp + raf.seek(index * 4 + 4096); + raf.writeInt(changeLastUpdate ? timestamp : chunk.getLastMCAUpdate()); + + globalOffset += sectors; + } + } + + // padding + if (lastWritten % 4096 != 0) { + raf.seek(globalOffset * 4096 - 1); + raf.write(0); + } + return chunksWritten; + } + + /** + * Set a specific Chunk at a specific index. The index must be in range of 0 - 1023. + * @param index The index of the Chunk. + * @param chunk The Chunk to be set. + * @throws IndexOutOfBoundsException If index is not in the range. + */ + public void setChunk(int index, Chunk chunk) { + checkIndex(index); + if (chunks == null) { + chunks = new Chunk[1024]; + } + chunks[index] = chunk; + } + + /** + * Set a specific Chunk at a specific chunk location. + * The x- and z-value can be absolute chunk coordinates or they can be relative to the region origin. + * @param chunkX The x-coordinate of the Chunk. + * @param chunkZ The z-coordinate of the Chunk. + * @param chunk The chunk to be set. + */ + public void setChunk(int chunkX, int chunkZ, Chunk chunk) { + setChunk(getChunkIndex(chunkX, chunkZ), chunk); + } + + /** + * Returns the chunk data of a chunk at a specific index in this file. + * @param index The index of the chunk in this file. + * @return The chunk data. + * */ + public Chunk getChunk(int index) { + checkIndex(index); + if (chunks == null) { + return null; + } + return chunks[index]; + } + + /** + * Returns the chunk data of a chunk in this file. + * @param chunkX The x-coordinate of the chunk. + * @param chunkZ The z-coordinate of the chunk. + * @return The chunk data. + * */ + public Chunk getChunk(int chunkX, int chunkZ) { + return getChunk(getChunkIndex(chunkX, chunkZ)); + } + + /** + * Calculates the index of a chunk from its x- and z-coordinates in this region. + * This works with absolute and relative coordinates. + * @param chunkX The x-coordinate of the chunk. + * @param chunkZ The z-coordinate of the chunk. + * @return The index of this chunk. + * */ + public static int getChunkIndex(int chunkX, int chunkZ) { + return (chunkX & 0x1F) + (chunkZ & 0x1F) * 32; + } + + private int checkIndex(int index) { + if (index < 0 || index > 1023) { + throw new IndexOutOfBoundsException(); + } + return index; + } + + private Chunk createChunkIfMissing(int blockX, int blockZ) { + int chunkX = MCAUtil.blockToChunk(blockX), chunkZ = MCAUtil.blockToChunk(blockZ); + Chunk chunk = getChunk(chunkX, chunkZ); + if (chunk == null) { + chunk = Chunk.newChunk(); + setChunk(getChunkIndex(chunkX, chunkZ), chunk); + } + return chunk; + } + + /** + * @deprecated Use {@link #setBiomeAt(int, int, int, int)} instead + */ + @Deprecated + public void setBiomeAt(int blockX, int blockZ, int biomeID) { + createChunkIfMissing(blockX, blockZ).setBiomeAt(blockX, blockZ, biomeID); + } + + public void setBiomeAt(int blockX, int blockY, int blockZ, int biomeID) { + createChunkIfMissing(blockX, blockZ).setBiomeAt(blockX, blockY, blockZ, biomeID); + } + + /** + * @deprecated Use {@link #getBiomeAt(int, int, int)} instead + */ + @Deprecated + public int getBiomeAt(int blockX, int blockZ) { + int chunkX = MCAUtil.blockToChunk(blockX), chunkZ = MCAUtil.blockToChunk(blockZ); + Chunk chunk = getChunk(getChunkIndex(chunkX, chunkZ)); + if (chunk == null) { + return -1; + } + return chunk.getBiomeAt(blockX, blockZ); + } + + /** + * Fetches the biome id at a specific block. + * @param blockX The x-coordinate of the block. + * @param blockY The y-coordinate of the block. + * @param blockZ The z-coordinate of the block. + * @return The biome id if the chunk exists and the chunk has biomes, otherwise -1. + */ + public int getBiomeAt(int blockX, int blockY, int blockZ) { + int chunkX = MCAUtil.blockToChunk(blockX), chunkZ = MCAUtil.blockToChunk(blockZ); + Chunk chunk = getChunk(getChunkIndex(chunkX, chunkZ)); + if (chunk == null) { + return -1; + } + return chunk.getBiomeAt(blockX,blockY, blockZ); + } + + /** + * Set a block state at a specific block location. + * The block coordinates can be absolute coordinates or they can be relative to the region. + * @param blockX The x-coordinate of the block. + * @param blockY The y-coordinate of the block. + * @param blockZ The z-coordinate of the block. + * @param state The block state to be set. + * @param cleanup Whether the Palette and the BLockStates should be recalculated after adding the block state. + */ + public void setBlockStateAt(int blockX, int blockY, int blockZ, CompoundTag state, boolean cleanup) { + createChunkIfMissing(blockX, blockZ).setBlockStateAt(blockX, blockY, blockZ, state, cleanup); + } + + /** + * Fetches a block state at a specific block location. + * The block coordinates can be absolute coordinates or they can be relative to the region. + * @param blockX The x-coordinate of the block. + * @param blockY The y-coordinate of the block. + * @param blockZ The z-coordinate of the block. + * @return The block state or null if the chunk or the section do not exist. + */ + public CompoundTag getBlockStateAt(int blockX, int blockY, int blockZ) { + int chunkX = MCAUtil.blockToChunk(blockX), chunkZ = MCAUtil.blockToChunk(blockZ); + Chunk chunk = getChunk(chunkX, chunkZ); + if (chunk == null) { + return null; + } + return chunk.getBlockStateAt(blockX, blockY, blockZ); + } + + /** + * Recalculates the Palette and the BlockStates of all chunks and sections of this region. + */ + public void cleanupPalettesAndBlockStates() { + for (Chunk chunk : chunks) { + if (chunk != null) { + chunk.cleanupPalettesAndBlockStates(); + } + } + } +} diff --git a/src/main/java/net/querz/mca/MCAUtil.java b/src/main/java/net/querz/mca/MCAUtil.java new file mode 100644 index 000000000..f5ddecc5a --- /dev/null +++ b/src/main/java/net/querz/mca/MCAUtil.java @@ -0,0 +1,223 @@ +package net.querz.mca; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Provides main and utility functions to read and write .mca files and + * to convert block, chunk and region coordinates. + * */ +public final class MCAUtil { + + private MCAUtil() {} + + /** + * @see MCAUtil#read(File) + * @param file The file to read the data from. + * @return An in-memory representation of the MCA file with decompressed chunk data. + * @throws IOException if something during deserialization goes wrong. + * */ + public static MCAFile read(String file) throws IOException { + return read(new File(file), LoadFlags.ALL_DATA); + } + + /** + * Reads an MCA file and loads all of its chunks. + * @param file The file to read the data from. + * @return An in-memory representation of the MCA file with decompressed chunk data. + * @throws IOException if something during deserialization goes wrong. + * */ + public static MCAFile read(File file) throws IOException { + return read(file, LoadFlags.ALL_DATA); + } + + /** + * @see MCAUtil#read(File) + * @param file The file to read the data from. + * @return An in-memory representation of the MCA file with decompressed chunk data. + * @param loadFlags A logical or of {@link LoadFlags} constants indicating what data should be loaded + * @throws IOException if something during deserialization goes wrong. + * */ + public static MCAFile read(String file, long loadFlags) throws IOException { + return read(new File(file), loadFlags); + } + + /** + * Reads an MCA file and loads all of its chunks. + * @param file The file to read the data from. + * @return An in-memory representation of the MCA file with decompressed chunk data + * @param loadFlags A logical or of {@link LoadFlags} constants indicating what data should be loaded + * @throws IOException if something during deserialization goes wrong. + * */ + public static MCAFile read(File file, long loadFlags) throws IOException { + MCAFile mcaFile = newMCAFile(file); + try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { + mcaFile.deserialize(raf, loadFlags); + return mcaFile; + } + } + + /** + * Calls {@link MCAUtil#write(MCAFile, File, boolean)} without changing the timestamps. + * @see MCAUtil#write(MCAFile, File, boolean) + * @param file The file to write to. + * @param mcaFile The data of the MCA file to write. + * @return The amount of chunks written to the file. + * @throws IOException If something goes wrong during serialization. + * */ + public static int write(MCAFile mcaFile, String file) throws IOException { + return write(mcaFile, new File(file), false); + } + + /** + * Calls {@link MCAUtil#write(MCAFile, File, boolean)} without changing the timestamps. + * @see MCAUtil#write(MCAFile, File, boolean) + * @param file The file to write to. + * @param mcaFile The data of the MCA file to write. + * @return The amount of chunks written to the file. + * @throws IOException If something goes wrong during serialization. + * */ + public static int write(MCAFile mcaFile, File file) throws IOException { + return write(mcaFile, file, false); + } + + /** + * @see MCAUtil#write(MCAFile, File, boolean) + * @param file The file to write to. + * @param mcaFile The data of the MCA file to write. + * @param changeLastUpdate Whether to adjust the timestamps of when the file was saved. + * @return The amount of chunks written to the file. + * @throws IOException If something goes wrong during serialization. + * */ + public static int write(MCAFile mcaFile, String file, boolean changeLastUpdate) throws IOException { + return write(mcaFile, new File(file), changeLastUpdate); + } + + /** + * Writes an {@code MCAFile} object to disk. It optionally adjusts the timestamps + * when the file was last saved to the current date and time or leaves them at + * the value set by either loading an already existing MCA file or setting them manually.
+ * If the file already exists, it is completely overwritten by the new file (no modification). + * @param file The file to write to. + * @param mcaFile The data of the MCA file to write. + * @param changeLastUpdate Whether to adjust the timestamps of when the file was saved. + * @return The amount of chunks written to the file. + * @throws IOException If something goes wrong during serialization. + * */ + public static int write(MCAFile mcaFile, File file, boolean changeLastUpdate) throws IOException { + File to = file; + if (file.exists()) { + to = File.createTempFile(to.getName(), null); + } + int chunks; + try (RandomAccessFile raf = new RandomAccessFile(to, "rw")) { + chunks = mcaFile.serialize(raf, changeLastUpdate); + } + + if (chunks > 0 && to != file) { + Files.move(to.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + return chunks; + } + + /** + * Turns the chunks coordinates into region coordinates and calls + * {@link MCAUtil#createNameFromRegionLocation(int, int)} + * @param chunkX The x-value of the location of the chunk. + * @param chunkZ The z-value of the location of the chunk. + * @return A mca filename in the format "r.{regionX}.{regionZ}.mca" + * */ + public static String createNameFromChunkLocation(int chunkX, int chunkZ) { + return createNameFromRegionLocation( chunkToRegion(chunkX), chunkToRegion(chunkZ)); + } + + /** + * Turns the block coordinates into region coordinates and calls + * {@link MCAUtil#createNameFromRegionLocation(int, int)} + * @param blockX The x-value of the location of the block. + * @param blockZ The z-value of the location of the block. + * @return A mca filename in the format "r.{regionX}.{regionZ}.mca" + * */ + public static String createNameFromBlockLocation(int blockX, int blockZ) { + return createNameFromRegionLocation(blockToRegion(blockX), blockToRegion(blockZ)); + } + + /** + * Creates a filename string from provided chunk coordinates. + * @param regionX The x-value of the location of the region. + * @param regionZ The z-value of the location of the region. + * @return A mca filename in the format "r.{regionX}.{regionZ}.mca" + * */ + public static String createNameFromRegionLocation(int regionX, int regionZ) { + return "r." + regionX + "." + regionZ + ".mca"; + } + + /** + * Turns a block coordinate value into a chunk coordinate value. + * @param block The block coordinate value. + * @return The chunk coordinate value. + * */ + public static int blockToChunk(int block) { + return block >> 4; + } + + /** + * Turns a block coordinate value into a region coordinate value. + * @param block The block coordinate value. + * @return The region coordinate value. + * */ + public static int blockToRegion(int block) { + return block >> 9; + } + + /** + * Turns a chunk coordinate value into a region coordinate value. + * @param chunk The chunk coordinate value. + * @return The region coordinate value. + * */ + public static int chunkToRegion(int chunk) { + return chunk >> 5; + } + + /** + * Turns a region coordinate value into a chunk coordinate value. + * @param region The region coordinate value. + * @return The chunk coordinate value. + * */ + public static int regionToChunk(int region) { + return region << 5; + } + + /** + * Turns a region coordinate value into a block coordinate value. + * @param region The region coordinate value. + * @return The block coordinate value. + * */ + public static int regionToBlock(int region) { + return region << 9; + } + + /** + * Turns a chunk coordinate value into a block coordinate value. + * @param chunk The chunk coordinate value. + * @return The block coordinate value. + * */ + public static int chunkToBlock(int chunk) { + return chunk << 4; + } + + private static final Pattern mcaFilePattern = Pattern.compile("^.*r\\.(?-?\\d+)\\.(?-?\\d+)\\.mca$"); + + public static MCAFile newMCAFile(File file) { + Matcher m = mcaFilePattern.matcher(file.getName()); + if (m.find()) { + return new MCAFile(Integer.parseInt(m.group("regionX")), Integer.parseInt(m.group("regionZ"))); + } + throw new IllegalArgumentException("invalid mca file name: " + file.getName()); + } +} diff --git a/src/main/java/net/querz/mca/Section.java b/src/main/java/net/querz/mca/Section.java new file mode 100644 index 000000000..312035a39 --- /dev/null +++ b/src/main/java/net/querz/mca/Section.java @@ -0,0 +1,398 @@ +package net.querz.mca; + +import static net.querz.mca.LoadFlags.*; +import net.querz.nbt.tag.ByteArrayTag; +import net.querz.nbt.tag.CompoundTag; +import net.querz.nbt.tag.ListTag; +import net.querz.nbt.tag.LongArrayTag; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class Section { + + private CompoundTag data; + private Map> valueIndexedPalette = new HashMap<>(); + private ListTag palette; + private byte[] blockLight; + private long[] blockStates; + private byte[] skyLight; + private int dataVersion; + + public Section(CompoundTag sectionRoot, int dataVersion) { + this(sectionRoot, dataVersion, ALL_DATA); + } + + public Section(CompoundTag sectionRoot, int dataVersion, long loadFlags) { + data = sectionRoot; + this.dataVersion = dataVersion; + ListTag rawPalette = sectionRoot.getListTag("Palette"); + if (rawPalette == null) { + return; + } + palette = rawPalette.asCompoundTagList(); + for (int i = 0; i < palette.size(); i++) { + CompoundTag data = palette.get(i); + putValueIndexedPalette(data, i); + } + + ByteArrayTag blockLight = sectionRoot.getByteArrayTag("BlockLight"); + LongArrayTag blockStates = sectionRoot.getLongArrayTag("BlockStates"); + ByteArrayTag skyLight = sectionRoot.getByteArrayTag("SkyLight"); + + if ((loadFlags & BLOCK_LIGHTS) != 0) { + this.blockLight = blockLight != null ? blockLight.getValue() : null; + } + if ((loadFlags & BLOCK_STATES) != 0) { + this.blockStates = blockStates != null ? blockStates.getValue() : null; + } + if ((loadFlags & SKY_LIGHT) != 0) { + this.skyLight = skyLight != null ? skyLight.getValue() : null; + } + } + + Section() {} + + void putValueIndexedPalette(CompoundTag data, int index) { + PaletteIndex leaf = new PaletteIndex(data, index); + String name = data.getString("Name"); + List leaves = valueIndexedPalette.get(name); + if (leaves == null) { + leaves = new ArrayList<>(1); + leaves.add(leaf); + valueIndexedPalette.put(name, leaves); + } else { + for (PaletteIndex pal : leaves) { + if (pal.data.equals(data)) { + return; + } + } + leaves.add(leaf); + } + } + + PaletteIndex getValueIndexedPalette(CompoundTag data) { + List leaves = valueIndexedPalette.get(data.getString("Name")); + if (leaves == null) { + return null; + } + for (PaletteIndex leaf : leaves) { + if (leaf.data.equals(data)) { + return leaf; + } + } + return null; + } + + private static class PaletteIndex { + + CompoundTag data; + int index; + + PaletteIndex(CompoundTag data, int index) { + this.data = data; + this.index = index; + } + } + + /** + * Checks whether the data of this Section is empty. + * @return true if empty + */ + public boolean isEmpty() { + return data == null; + } + + /** + * Fetches a block state based on a block location from this section. + * The coordinates represent the location of the block inside of this Section. + * @param blockX The x-coordinate of the block in this Section + * @param blockY The y-coordinate of the block in this Section + * @param blockZ The z-coordinate of the block in this Section + * @return The block state data of this block. + */ + public CompoundTag getBlockStateAt(int blockX, int blockY, int blockZ) { + int index = getBlockIndex(blockX, blockY, blockZ); + int paletteIndex = getPaletteIndex(index); + return palette.get(paletteIndex); + } + + /** + * Attempts to add a block state for a specific block location in this Section. + * @param blockX The x-coordinate of the block in this Section + * @param blockY The y-coordinate of the block in this Section + * @param blockZ The z-coordinate of the block in this Section + * @param state The block state to be set + * @param cleanup When true, it will cleanup the palette of this section. + * This option should only be used moderately to avoid unnecessary recalculation of the palette indices. + * Recalculating the Palette should only be executed once right before saving the Section to file. + */ + public void setBlockStateAt(int blockX, int blockY, int blockZ, CompoundTag state, boolean cleanup) { + int paletteSizeBefore = palette.size(); + int paletteIndex = addToPalette(state); + //power of 2 --> bits must increase, but only if the palette size changed + //otherwise we would attempt to update all blockstates and the entire palette + //every time an existing blockstate was added while having 2^x blockstates in the palette + if (paletteSizeBefore != palette.size() && (paletteIndex & (paletteIndex - 1)) == 0) { + adjustBlockStateBits(null, blockStates); + cleanup = true; + } + + setPaletteIndex(getBlockIndex(blockX, blockY, blockZ), paletteIndex, blockStates); + + if (cleanup) { + cleanupPaletteAndBlockStates(); + } + } + + /** + * Returns the index of the block data in the palette. + * @param blockStateIndex The index of the block in this section, ranging from 0-4095. + * @return The index of the block data in the palette. + * */ + public int getPaletteIndex(int blockStateIndex) { + int bits = blockStates.length >> 6; + + if (dataVersion < 2527) { + double blockStatesIndex = blockStateIndex / (4096D / blockStates.length); + int longIndex = (int) blockStatesIndex; + int startBit = (int) ((blockStatesIndex - Math.floor(blockStatesIndex)) * 64D); + if (startBit + bits > 64) { + long prev = bitRange(blockStates[longIndex], startBit, 64); + long next = bitRange(blockStates[longIndex + 1], 0, startBit + bits - 64); + return (int) ((next << 64 - startBit) + prev); + } else { + return (int) bitRange(blockStates[longIndex], startBit, startBit + bits); + } + } else { + int indicesPerLong = (int) (64D / bits); + int blockStatesIndex = blockStateIndex / indicesPerLong; + int startBit = (blockStateIndex % indicesPerLong) * bits; + return (int) bitRange(blockStates[blockStatesIndex], startBit, startBit + bits); + } + } + + /** + * Sets the index of the block data in the BlockStates. Does not adjust the size of the BlockStates array. + * @param blockIndex The index of the block in this section, ranging from 0-4095. + * @param paletteIndex The block state to be set (index of block data in the palette). + * @param blockStates The block states to be updated. + * */ + public void setPaletteIndex(int blockIndex, int paletteIndex, long[] blockStates) { + int bits = blockStates.length >> 6; + + if (dataVersion < 2527) { + double blockStatesIndex = blockIndex / (4096D / blockStates.length); + int longIndex = (int) blockStatesIndex; + int startBit = (int) ((blockStatesIndex - Math.floor(longIndex)) * 64D); + if (startBit + bits > 64) { + blockStates[longIndex] = updateBits(blockStates[longIndex], paletteIndex, startBit, 64); + blockStates[longIndex + 1] = updateBits(blockStates[longIndex + 1], paletteIndex, startBit - 64, startBit + bits - 64); + } else { + blockStates[longIndex] = updateBits(blockStates[longIndex], paletteIndex, startBit, startBit + bits); + } + } else { + int indicesPerLong = (int) (64D / bits); + int blockStatesIndex = blockIndex / indicesPerLong; + int startBit = (blockIndex % indicesPerLong) * bits; + blockStates[blockStatesIndex] = updateBits(blockStates[blockStatesIndex], paletteIndex, startBit, startBit + bits); + } + } + + /** + * Fetches the palette of this Section. + * @return The palette of this Section. + */ + public ListTag getPalette() { + return palette; + } + + int addToPalette(CompoundTag data) { + PaletteIndex index; + if ((index = getValueIndexedPalette(data)) != null) { + return index.index; + } + palette.add(data); + putValueIndexedPalette(data, palette.size() - 1); + return palette.size() - 1; + } + + int getBlockIndex(int blockX, int blockY, int blockZ) { + return (blockY & 0xF) * 256 + (blockZ & 0xF) * 16 + (blockX & 0xF); + } + + static long updateBits(long n, long m, int i, int j) { + //replace i to j in n with j - i bits of m + long mShifted = i > 0 ? (m & ((1L << j - i) - 1)) << i : (m & ((1L << j - i) - 1)) >>> -i; + return ((n & ((j > 63 ? 0 : (~0L << j)) | (i < 0 ? 0 : ((1L << i) - 1L)))) | mShifted); + } + + static long bitRange(long value, int from, int to) { + int waste = 64 - to; + return (value << waste) >>> (waste + from); + } + + /** + * This method recalculates the palette and its indices. + * This should only be used moderately to avoid unnecessary recalculation of the palette indices. + * Recalculating the Palette should only be executed once right before saving the Section to file. + */ + public void cleanupPaletteAndBlockStates() { + Map oldToNewMapping = cleanupPalette(); + adjustBlockStateBits(oldToNewMapping, blockStates); + } + + private Map cleanupPalette() { + //create index - palette mapping + Map allIndices = new HashMap<>(); + for (int i = 0; i < 4096; i++) { + int paletteIndex = getPaletteIndex(i); + allIndices.put(paletteIndex, paletteIndex); + } + //delete unused blocks from palette + //start at index 1 because we need to keep minecraft:air + int index = 1; + valueIndexedPalette = new HashMap<>(valueIndexedPalette.size()); + putValueIndexedPalette(palette.get(0), 0); + for (int i = 1; i < palette.size(); i++) { + if (!allIndices.containsKey(index)) { + palette.remove(i); + i--; + } else { + putValueIndexedPalette(palette.get(i), i); + allIndices.put(index, i); + } + index++; + } + + return allIndices; + } + + void adjustBlockStateBits(Map oldToNewMapping, long[] blockStates) { + //increases or decreases the amount of bits used per BlockState + //based on the size of the palette. oldToNewMapping can be used to update indices + //if the palette had been cleaned up before using MCAFile#cleanupPalette(). + + int newBits = 32 - Integer.numberOfLeadingZeros(palette.size() - 1); + newBits = Math.max(newBits, 4); + + long[] newBlockStates; + + if (dataVersion < 2527) { + newBlockStates = newBits == blockStates.length / 64 ? blockStates : new long[newBits * 64]; + } else { + int newLength = (int) Math.ceil(4096D / (64D / newBits)); + newBlockStates = newBits == blockStates.length / 64 ? blockStates : new long[newLength]; + } + if (oldToNewMapping != null) { + for (int i = 0; i < 4096; i++) { + setPaletteIndex(i, oldToNewMapping.get(getPaletteIndex(i)), newBlockStates); + } + } else { + for (int i = 0; i < 4096; i++) { + setPaletteIndex(i, getPaletteIndex(i), newBlockStates); + } + } + this.blockStates = newBlockStates; + } + + /** + * @return The block light array of this Section + */ + public byte[] getBlockLight() { + return blockLight; + } + + /** + * Sets the block light array for this section. + * @param blockLight The block light array + * @throws IllegalArgumentException When the length of the array is not 2048 + */ + public void setBlockLight(byte[] blockLight) { + if (blockLight != null && blockLight.length != 2048) { + throw new IllegalArgumentException("BlockLight array must have a length of 2048"); + } + this.blockLight = blockLight; + } + + /** + * @return The indices of the block states of this Section. + */ + public long[] getBlockStates() { + return blockStates; + } + + /** + * Sets the block state indices to a custom value. + * @param blockStates The block state indices. + * @throws NullPointerException If blockStates is null + * @throws IllegalArgumentException When blockStates' length is < 256 or > 4096 and is not a multiple of 64 + */ + public void setBlockStates(long[] blockStates) { + if (blockStates == null) { + throw new NullPointerException("BlockStates cannot be null"); + } else if (blockStates.length % 64 != 0 || blockStates.length < 256 || blockStates.length > 4096) { + throw new IllegalArgumentException("BlockStates must have a length > 255 and < 4097 and must be divisible by 64"); + } + this.blockStates = blockStates; + } + + /** + * @return The sky light values of this Section + */ + public byte[] getSkyLight() { + return skyLight; + } + + /** + * Sets the sky light values of this section. + * @param skyLight The custom sky light values + * @throws IllegalArgumentException If the length of the array is not 2048 + */ + public void setSkyLight(byte[] skyLight) { + if (skyLight != null && skyLight.length != 2048) { + throw new IllegalArgumentException("SkyLight array must have a length of 2048"); + } + this.skyLight = skyLight; + } + + /** + * Creates an empty Section with base values. + * @return An empty Section + */ + public static Section newSection() { + Section s = new Section(); + s.blockStates = new long[256]; + s.palette = new ListTag<>(CompoundTag.class); + CompoundTag air = new CompoundTag(); + air.putString("Name", "minecraft:air"); + s.palette.add(air); + s.data = new CompoundTag(); + return s; + } + + /** + * Updates the raw CompoundTag that this Section is based on. + * This must be called before saving a Section to disk if the Section was manually created + * to set the Y of this Section. + * @param y The Y-value of this Section + * @return A reference to the raw CompoundTag this Section is based on + */ + public CompoundTag updateHandle(int y) { + data.putByte("Y", (byte) y); + if (palette != null) { + data.put("Palette", palette); + } + if (blockLight != null) { + data.putByteArray("BlockLight", blockLight); + } + if (blockStates != null) { + data.putLongArray("BlockStates", blockStates); + } + if (skyLight != null) { + data.putByteArray("SkyLight", skyLight); + } + return data; + } +} diff --git a/src/main/java/net/querz/nbt/io/NBTDeserializer.java b/src/main/java/net/querz/nbt/io/NBTDeserializer.java new file mode 100644 index 000000000..2f7289e32 --- /dev/null +++ b/src/main/java/net/querz/nbt/io/NBTDeserializer.java @@ -0,0 +1,31 @@ +package net.querz.nbt.io; + +import net.querz.io.Deserializer; +import net.querz.nbt.tag.Tag; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.GZIPInputStream; + +public class NBTDeserializer implements Deserializer { + + private boolean compressed; + + public NBTDeserializer() { + this(true); + } + + public NBTDeserializer(boolean compressed) { + this.compressed = compressed; + } + + @Override + public NamedTag fromStream(InputStream stream) throws IOException { + NBTInputStream nbtIn; + if (compressed) { + nbtIn = new NBTInputStream(new GZIPInputStream(stream)); + } else { + nbtIn = new NBTInputStream(stream); + } + return nbtIn.readTag(Tag.DEFAULT_MAX_DEPTH); + } +} diff --git a/src/main/java/net/querz/nbt/io/NBTInputStream.java b/src/main/java/net/querz/nbt/io/NBTInputStream.java new file mode 100644 index 000000000..b3ca6b8ae --- /dev/null +++ b/src/main/java/net/querz/nbt/io/NBTInputStream.java @@ -0,0 +1,149 @@ +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.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +public class NBTInputStream extends DataInputStream implements MaxDepthIO { + + 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, NBTInputStream::readListTag, ListTag.class); + put(CompoundTag.ID, NBTInputStream::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 NBTInputStream(InputStream in) { + super(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(NBTInputStream in) throws IOException { + return new ByteTag(in.readByte()); + } + + private static ShortTag readShort(NBTInputStream in) throws IOException { + return new ShortTag(in.readShort()); + } + + private static IntTag readInt(NBTInputStream in) throws IOException { + return new IntTag(in.readInt()); + } + + private static LongTag readLong(NBTInputStream in) throws IOException { + return new LongTag(in.readLong()); + } + + private static FloatTag readFloat(NBTInputStream in) throws IOException { + return new FloatTag(in.readFloat()); + } + + private static DoubleTag readDouble(NBTInputStream in) throws IOException { + return new DoubleTag(in.readDouble()); + } + + private static StringTag readString(NBTInputStream in) throws IOException { + return new StringTag(in.readUTF()); + } + + private static ByteArrayTag readByteArray(NBTInputStream in) throws IOException { + ByteArrayTag bat = new ByteArrayTag(new byte[in.readInt()]); + in.readFully(bat.getValue()); + return bat; + } + + private static IntArrayTag readIntArray(NBTInputStream 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(NBTInputStream 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(NBTInputStream 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(NBTInputStream 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; + } +} diff --git a/src/main/java/net/querz/nbt/io/NBTOutputStream.java b/src/main/java/net/querz/nbt/io/NBTOutputStream.java new file mode 100644 index 000000000..4879052ef --- /dev/null +++ b/src/main/java/net/querz/nbt/io/NBTOutputStream.java @@ -0,0 +1,153 @@ +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.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; + +public class NBTOutputStream extends DataOutputStream implements MaxDepthIO { + + 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, NBTOutputStream::writeList, ListTag.class); + put(CompoundTag.ID, NBTOutputStream::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 NBTOutputStream(OutputStream out) { + super(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(NBTOutputStream out, Tag tag) throws IOException { + out.writeByte(((ByteTag) tag).asByte()); + } + + private static void writeShort(NBTOutputStream out, Tag tag) throws IOException { + out.writeShort(((ShortTag) tag).asShort()); + } + + private static void writeInt(NBTOutputStream out, Tag tag) throws IOException { + out.writeInt(((IntTag) tag).asInt()); + } + + private static void writeLong(NBTOutputStream out, Tag tag) throws IOException { + out.writeLong(((LongTag) tag).asLong()); + } + + private static void writeFloat(NBTOutputStream out, Tag tag) throws IOException { + out.writeFloat(((FloatTag) tag).asFloat()); + } + + private static void writeDouble(NBTOutputStream out, Tag tag) throws IOException { + out.writeDouble(((DoubleTag) tag).asDouble()); + } + + private static void writeString(NBTOutputStream out, Tag tag) throws IOException { + out.writeUTF(((StringTag) tag).getValue()); + } + + private static void writeByteArray(NBTOutputStream out, Tag tag) throws IOException { + out.writeInt(((ByteArrayTag) tag).length()); + out.write(((ByteArrayTag) tag).getValue()); + } + + private static void writeIntArray(NBTOutputStream out, Tag tag) throws IOException { + out.writeInt(((IntArrayTag) tag).length()); + for (int i : ((IntArrayTag) tag).getValue()) { + out.writeInt(i); + } + } + + private static void writeLongArray(NBTOutputStream out, Tag tag) throws IOException { + out.writeInt(((LongArrayTag) tag).length()); + for (long l : ((LongArrayTag) tag).getValue()) { + out.writeLong(l); + } + } + + private static void writeList(NBTOutputStream 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(NBTOutputStream 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); + } +} diff --git a/src/main/java/net/querz/nbt/io/NBTSerializer.java b/src/main/java/net/querz/nbt/io/NBTSerializer.java new file mode 100644 index 000000000..fe5059926 --- /dev/null +++ b/src/main/java/net/querz/nbt/io/NBTSerializer.java @@ -0,0 +1,32 @@ +package net.querz.nbt.io; + +import net.querz.io.Serializer; +import net.querz.nbt.tag.Tag; +import java.io.IOException; +import java.io.OutputStream; +import java.util.zip.GZIPOutputStream; + +public class NBTSerializer implements Serializer { + + private boolean compressed; + + public NBTSerializer() { + this(true); + } + + public NBTSerializer(boolean compressed) { + this.compressed = compressed; + } + + @Override + public void toStream(NamedTag object, OutputStream out) throws IOException { + NBTOutputStream nbtOut; + if (compressed) { + nbtOut = new NBTOutputStream(new GZIPOutputStream(out, true)); + } else { + nbtOut = new NBTOutputStream(out); + } + 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 new file mode 100644 index 000000000..d8efc150b --- /dev/null +++ b/src/main/java/net/querz/nbt/io/NBTUtil.java @@ -0,0 +1,80 @@ +package net.querz.nbt.io; + +import net.querz.nbt.tag.Tag; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PushbackInputStream; +import java.util.zip.GZIPInputStream; + +public final class NBTUtil { + + private NBTUtil() {} + + public static void write(NamedTag tag, File file, boolean compressed) throws IOException { + try (FileOutputStream fos = new FileOutputStream(file)) { + new NBTSerializer(compressed).toStream(tag, fos); + } + } + + public static void write(NamedTag tag, String file, boolean compressed) throws IOException { + write(tag, new File(file), compressed); + } + + public static void write(NamedTag tag, File file) throws IOException { + write(tag, file, true); + } + + public static void write(NamedTag tag, String file) throws IOException { + write(tag, new File(file), true); + } + + public static void write(Tag tag, File file, boolean compressed) throws IOException { + write(new NamedTag(null, tag), file, compressed); + } + + public static void write(Tag tag, String file, boolean compressed) throws IOException { + write(new NamedTag(null, tag), new File(file), compressed); + } + + public static void write(Tag tag, File file) throws IOException { + write(new NamedTag(null, tag), file, true); + } + + public static void write(Tag tag, String file) throws IOException { + write(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); + } + } + + public static NamedTag read(String file, boolean compressed) throws IOException { + return read(new File(file), compressed); + } + + public static NamedTag read(File file) throws IOException { + try (FileInputStream fis = new FileInputStream(file)) { + return new NBTDeserializer(false).fromStream(detectDecompression(fis)); + } + } + + public static NamedTag read(String file) throws IOException { + return read(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); + pbis.unread(signature >> 8); + pbis.unread(signature & 0xFF); + if (signature == GZIPInputStream.GZIP_MAGIC) { + return new GZIPInputStream(pbis); + } + return pbis; + } +} diff --git a/src/main/java/net/querz/nbt/io/NamedTag.java b/src/main/java/net/querz/nbt/io/NamedTag.java new file mode 100644 index 000000000..b1873087a --- /dev/null +++ b/src/main/java/net/querz/nbt/io/NamedTag.java @@ -0,0 +1,30 @@ +package net.querz.nbt.io; + +import net.querz.nbt.tag.Tag; + +public class NamedTag { + + private String name; + private Tag tag; + + public NamedTag(String name, Tag tag) { + this.name = name; + this.tag = tag; + } + + public void setName(String name) { + this.name = name; + } + + public void setTag(Tag tag) { + this.tag = tag; + } + + public String getName() { + return name; + } + + public Tag getTag() { + return tag; + } +} diff --git a/src/main/java/net/querz/nbt/io/ParseException.java b/src/main/java/net/querz/nbt/io/ParseException.java new file mode 100644 index 000000000..c62e0610c --- /dev/null +++ b/src/main/java/net/querz/nbt/io/ParseException.java @@ -0,0 +1,25 @@ +package net.querz.nbt.io; + +import java.io.IOException; + +public class ParseException extends IOException { + + public ParseException(String msg) { + super(msg); + } + + public ParseException(String msg, String value, int index) { + super(msg + " at: " + formatError(value, index)); + } + + private static String formatError(String value, int index) { + StringBuilder builder = new StringBuilder(); + int i = Math.min(value.length(), index); + if (i > 35) { + builder.append("..."); + } + builder.append(value, Math.max(0, i - 35), i); + builder.append("<--[HERE]"); + return builder.toString(); + } +} diff --git a/src/main/java/net/querz/nbt/io/SNBTDeserializer.java b/src/main/java/net/querz/nbt/io/SNBTDeserializer.java new file mode 100644 index 000000000..05a98fefc --- /dev/null +++ b/src/main/java/net/querz/nbt/io/SNBTDeserializer.java @@ -0,0 +1,26 @@ +package net.querz.nbt.io; + +import net.querz.io.StringDeserializer; +import net.querz.nbt.tag.Tag; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; +import java.util.stream.Collectors; + +public class SNBTDeserializer implements StringDeserializer> { + + @Override + public Tag fromReader(Reader reader) throws IOException { + return fromReader(reader, Tag.DEFAULT_MAX_DEPTH); + } + + public Tag fromReader(Reader reader, int maxDepth) throws IOException { + BufferedReader bufferedReader; + if (reader instanceof BufferedReader) { + bufferedReader = (BufferedReader) reader; + } else { + bufferedReader = new BufferedReader(reader); + } + return SNBTParser.parse(bufferedReader.lines().collect(Collectors.joining()), maxDepth); + } +} diff --git a/src/main/java/net/querz/nbt/io/SNBTParser.java b/src/main/java/net/querz/nbt/io/SNBTParser.java new file mode 100644 index 000000000..7a9ac5d26 --- /dev/null +++ b/src/main/java/net/querz/nbt/io/SNBTParser.java @@ -0,0 +1,244 @@ +package net.querz.nbt.io; + +import net.querz.io.MaxDepthIO; +import net.querz.nbt.tag.ArrayTag; +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.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +public final class SNBTParser implements MaxDepthIO { + + private static final Pattern + FLOAT_LITERAL_PATTERN = Pattern.compile("^[-+]?(?:\\d+\\.?|\\d*\\.\\d+)(?:e[-+]?\\d+)?f$", Pattern.CASE_INSENSITIVE), + DOUBLE_LITERAL_PATTERN = Pattern.compile("^[-+]?(?:\\d+\\.?|\\d*\\.\\d+)(?:e[-+]?\\d+)?d$", Pattern.CASE_INSENSITIVE), + DOUBLE_LITERAL_NO_SUFFIX_PATTERN = Pattern.compile("^[-+]?(?:\\d+\\.|\\d*\\.\\d+)(?:e[-+]?\\d+)?$", Pattern.CASE_INSENSITIVE), + BYTE_LITERAL_PATTERN = Pattern.compile("^[-+]?\\d+b$", Pattern.CASE_INSENSITIVE), + SHORT_LITERAL_PATTERN = Pattern.compile("^[-+]?\\d+s$", Pattern.CASE_INSENSITIVE), + INT_LITERAL_PATTERN = Pattern.compile("^[-+]?\\d+$", Pattern.CASE_INSENSITIVE), + LONG_LITERAL_PATTERN = Pattern.compile("^[-+]?\\d+l$", Pattern.CASE_INSENSITIVE), + NUMBER_PATTERN = Pattern.compile("^[-+]?\\d+$"); + + private StringPointer ptr; + + private SNBTParser(String string) { + this.ptr = new StringPointer(string); + } + + public static Tag parse(String string, int maxDepth) throws ParseException { + SNBTParser parser = new SNBTParser(string); + Tag tag = parser.parseAnything(maxDepth); + parser.ptr.skipWhitespace(); + if (parser.ptr.hasNext()) { + throw parser.ptr.parseException("invalid characters after end of snbt"); + } + return tag; + } + + public static Tag parse(String string) throws ParseException { + return parse(string, Tag.DEFAULT_MAX_DEPTH); + } + + private Tag parseAnything(int maxDepth) throws ParseException { + ptr.skipWhitespace(); + switch (ptr.currentChar()) { + case '{': + return parseCompoundTag(maxDepth); + case '[': + if (ptr.hasCharsLeft(2) && ptr.lookAhead(1) != '"' && ptr.lookAhead(2) == ';') { + return parseNumArray(); + } + return parseListTag(maxDepth); + } + return parseStringOrLiteral(); + } + + private Tag parseStringOrLiteral() throws ParseException { + ptr.skipWhitespace(); + if (ptr.currentChar() == '"') { + return new StringTag(ptr.parseQuotedString()); + } + String s = ptr.parseSimpleString(); + if (s.isEmpty()) { + throw new ParseException("expected non empty value"); + } + if (FLOAT_LITERAL_PATTERN.matcher(s).matches()) { + return new FloatTag(Float.parseFloat(s.substring(0, s.length() - 1))); + } else if (BYTE_LITERAL_PATTERN.matcher(s).matches()) { + try { + return new ByteTag(Byte.parseByte(s.substring(0, s.length() - 1))); + } catch (NumberFormatException ex) { + throw ptr.parseException("byte not in range: \"" + s.substring(0, s.length() - 1) + "\""); + } + } else if (SHORT_LITERAL_PATTERN.matcher(s).matches()) { + try { + return new ShortTag(Short.parseShort(s.substring(0, s.length() - 1))); + } catch (NumberFormatException ex) { + throw ptr.parseException("short not in range: \"" + s.substring(0, s.length() - 1) + "\""); + } + } else if (LONG_LITERAL_PATTERN.matcher(s).matches()) { + try { + return new LongTag(Long.parseLong(s.substring(0, s.length() - 1))); + } catch (NumberFormatException ex) { + throw ptr.parseException("long not in range: \"" + s.substring(0, s.length() - 1) + "\""); + } + } else if (INT_LITERAL_PATTERN.matcher(s).matches()) { + try { + return new IntTag(Integer.parseInt(s)); + } catch (NumberFormatException ex) { + throw ptr.parseException("int not in range: \"" + s.substring(0, s.length() - 1) + "\""); + } + } else if (DOUBLE_LITERAL_PATTERN.matcher(s).matches()) { + return new DoubleTag(Double.parseDouble(s.substring(0, s.length() - 1))); + } else if (DOUBLE_LITERAL_NO_SUFFIX_PATTERN.matcher(s).matches()) { + return new DoubleTag(Double.parseDouble(s)); + } else if ("true".equalsIgnoreCase(s)) { + return new ByteTag(true); + } else if ("false".equalsIgnoreCase(s)) { + return new ByteTag(false); + } + return new StringTag(s); + } + + private CompoundTag parseCompoundTag(int maxDepth) throws ParseException { + ptr.expectChar('{'); + + CompoundTag compoundTag = new CompoundTag(); + + ptr.skipWhitespace(); + while (ptr.hasNext() && ptr.currentChar() != '}') { + ptr.skipWhitespace(); + String key = ptr.currentChar() == '"' ? ptr.parseQuotedString() : ptr.parseSimpleString(); + if (key.isEmpty()) { + throw new ParseException("empty keys are not allowed"); + } + ptr.expectChar(':'); + + compoundTag.put(key, parseAnything(decrementMaxDepth(maxDepth))); + + if (!ptr.nextArrayElement()) { + break; + } + } + ptr.expectChar('}'); + return compoundTag; + } + + private ListTag parseListTag(int maxDepth) throws ParseException { + ptr.expectChar('['); + ptr.skipWhitespace(); + ListTag list = ListTag.createUnchecked(EndTag.class); + while (ptr.currentChar() != ']') { + Tag element = parseAnything(decrementMaxDepth(maxDepth)); + try { + list.addUnchecked(element); + } catch (IllegalArgumentException ex) { + throw ptr.parseException(ex.getMessage()); + } + if (!ptr.nextArrayElement()) { + break; + } + } + ptr.expectChar(']'); + return list; + } + + private ArrayTag parseNumArray() throws ParseException { + ptr.expectChar('['); + char arrayType = ptr.next(); + ptr.expectChar(';'); + ptr.skipWhitespace(); + switch (arrayType) { + case 'B': + return parseByteArrayTag(); + case 'I': + return parseIntArrayTag(); + case 'L': + return parseLongArrayTag(); + } + throw new ParseException("invalid array type '" + arrayType + "'"); + } + + private ByteArrayTag parseByteArrayTag() throws ParseException { + List byteList = new ArrayList<>(); + while (ptr.currentChar() != ']') { + String s = ptr.parseSimpleString(); + ptr.skipWhitespace(); + if (NUMBER_PATTERN.matcher(s).matches()) { + try { + byteList.add(Byte.parseByte(s)); + } catch (NumberFormatException ex) { + throw ptr.parseException("byte not in range: \"" + s + "\""); + } + } else { + throw ptr.parseException("invalid byte in ByteArrayTag: \"" + s + "\""); + } + if (!ptr.nextArrayElement()) { + break; + } + } + ptr.expectChar(']'); + byte[] bytes = new byte[byteList.size()]; + for (int i = 0; i < byteList.size(); i++) { + bytes[i] = byteList.get(i); + } + return new ByteArrayTag(bytes); + } + + private IntArrayTag parseIntArrayTag() throws ParseException { + List intList = new ArrayList<>(); + while (ptr.currentChar() != ']') { + String s = ptr.parseSimpleString(); + ptr.skipWhitespace(); + if (NUMBER_PATTERN.matcher(s).matches()) { + try { + intList.add(Integer.parseInt(s)); + } catch (NumberFormatException ex) { + throw ptr.parseException("int not in range: \"" + s + "\""); + } + } else { + throw ptr.parseException("invalid int in IntArrayTag: \"" + s + "\""); + } + if (!ptr.nextArrayElement()) { + break; + } + } + ptr.expectChar(']'); + return new IntArrayTag(intList.stream().mapToInt(i -> i).toArray()); + } + + private LongArrayTag parseLongArrayTag() throws ParseException { + List longList = new ArrayList<>(); + while (ptr.currentChar() != ']') { + String s = ptr.parseSimpleString(); + ptr.skipWhitespace(); + if (NUMBER_PATTERN.matcher(s).matches()) { + try { + longList.add(Long.parseLong(s)); + } catch (NumberFormatException ex) { + throw ptr.parseException("long not in range: \"" + s + "\""); + } + } else { + throw ptr.parseException("invalid long in LongArrayTag: \"" + s + "\""); + } + if (!ptr.nextArrayElement()) { + break; + } + } + ptr.expectChar(']'); + return new LongArrayTag(longList.stream().mapToLong(l -> l).toArray()); + } +} diff --git a/src/main/java/net/querz/nbt/io/SNBTSerializer.java b/src/main/java/net/querz/nbt/io/SNBTSerializer.java new file mode 100644 index 000000000..50ea44a63 --- /dev/null +++ b/src/main/java/net/querz/nbt/io/SNBTSerializer.java @@ -0,0 +1,18 @@ +package net.querz.nbt.io; + +import net.querz.io.StringSerializer; +import net.querz.nbt.tag.Tag; +import java.io.IOException; +import java.io.Writer; + +public class SNBTSerializer implements StringSerializer> { + + @Override + public void toWriter(Tag tag, Writer writer) throws IOException { + SNBTWriter.write(tag, writer); + } + + public void toWriter(Tag tag, Writer writer, int maxDepth) throws IOException { + SNBTWriter.write(tag, writer, maxDepth); + } +} diff --git a/src/main/java/net/querz/nbt/io/SNBTUtil.java b/src/main/java/net/querz/nbt/io/SNBTUtil.java new file mode 100644 index 000000000..fe87484a3 --- /dev/null +++ b/src/main/java/net/querz/nbt/io/SNBTUtil.java @@ -0,0 +1,15 @@ +package net.querz.nbt.io; + +import net.querz.nbt.tag.Tag; +import java.io.IOException; + +public class SNBTUtil { + + public static String toSNBT(Tag tag) throws IOException { + return new SNBTSerializer().toString(tag); + } + + public static Tag fromSNBT(String string) throws IOException { + return new SNBTDeserializer().fromString(string); + } +} diff --git a/src/main/java/net/querz/nbt/io/SNBTWriter.java b/src/main/java/net/querz/nbt/io/SNBTWriter.java new file mode 100644 index 000000000..c6cffcb70 --- /dev/null +++ b/src/main/java/net/querz/nbt/io/SNBTWriter.java @@ -0,0 +1,129 @@ +package net.querz.nbt.io; + +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.IOException; +import java.io.Writer; +import java.lang.reflect.Array; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * SNBTWriter creates an SNBT String. + * + * */ +public final class SNBTWriter implements MaxDepthIO { + + private static final Pattern NON_QUOTE_PATTERN = Pattern.compile("[a-zA-Z_.+\\-]+"); + + private Writer writer; + + private SNBTWriter(Writer writer) { + this.writer = writer; + } + + public static void write(Tag tag, Writer writer, int maxDepth) throws IOException { + new SNBTWriter(writer).writeAnything(tag, maxDepth); + } + + public static void write(Tag tag, Writer writer) throws IOException { + write(tag, writer, Tag.DEFAULT_MAX_DEPTH); + } + + private void writeAnything(Tag tag, int maxDepth) throws IOException { + switch (tag.getID()) { + case EndTag.ID: + //do nothing + break; + case ByteTag.ID: + writer.append(Byte.toString(((ByteTag) tag).asByte())).write('b'); + break; + case ShortTag.ID: + writer.append(Short.toString(((ShortTag) tag).asShort())).write('s'); + break; + case IntTag.ID: + writer.write(Integer.toString(((IntTag) tag).asInt())); + break; + case LongTag.ID: + writer.append(Long.toString(((LongTag) tag).asLong())).write('l'); + break; + case FloatTag.ID: + writer.append(Float.toString(((FloatTag) tag).asFloat())).write('f'); + break; + case DoubleTag.ID: + writer.append(Double.toString(((DoubleTag) tag).asDouble())).write('d'); + break; + case ByteArrayTag.ID: + writeArray(((ByteArrayTag) tag).getValue(), ((ByteArrayTag) tag).length(), "B"); + break; + case StringTag.ID: + writer.write(escapeString(((StringTag) tag).getValue())); + break; + case ListTag.ID: + writer.write('['); + for (int i = 0; i < ((ListTag) tag).size(); i++) { + writer.write(i == 0 ? "" : ","); + writeAnything(((ListTag) tag).get(i), decrementMaxDepth(maxDepth)); + } + writer.write(']'); + break; + case CompoundTag.ID: + writer.write('{'); + boolean first = true; + for (Map.Entry> entry : (CompoundTag) tag) { + writer.write(first ? "" : ","); + writer.append(escapeString(entry.getKey())).write(':'); + writeAnything(entry.getValue(), decrementMaxDepth(maxDepth)); + first = false; + } + writer.write('}'); + break; + case IntArrayTag.ID: + writeArray(((IntArrayTag) tag).getValue(), ((IntArrayTag) tag).length(), "I"); + break; + case LongArrayTag.ID: + writeArray(((LongArrayTag) tag).getValue(), ((LongArrayTag) tag).length(), "L"); + break; + default: + throw new IOException("unknown tag with id \"" + tag.getID() + "\""); + } + } + + private void writeArray(Object array, int length, String prefix) throws IOException { + writer.append('[').append(prefix).write(';'); + for (int i = 0; i < length; i++) { + writer.append(i == 0 ? "" : ",").write(Array.get(array, i).toString()); + } + writer.write(']'); + } + + public static String escapeString(String s) { + if (!NON_QUOTE_PATTERN.matcher(s).matches()) { + StringBuilder sb = new StringBuilder(); + sb.append('"'); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '\\' || c == '"') { + sb.append('\\'); + } + sb.append(c); + } + sb.append('"'); + return sb.toString(); + } + return s; + } +} diff --git a/src/main/java/net/querz/nbt/io/StringPointer.java b/src/main/java/net/querz/nbt/io/StringPointer.java new file mode 100644 index 000000000..11bdddcc4 --- /dev/null +++ b/src/main/java/net/querz/nbt/io/StringPointer.java @@ -0,0 +1,114 @@ +package net.querz.nbt.io; + +public class StringPointer { + + private String value; + private int index; + + public StringPointer(String value) { + this.value = value; + } + + public String parseSimpleString() { + int oldIndex = index; + while (hasNext() && isSimpleChar(currentChar())) { + index++; + } + return value.substring(oldIndex, index); + } + + public String parseQuotedString() throws ParseException { + int oldIndex = ++index; //ignore beginning quotes + StringBuilder sb = null; + boolean escape = false; + while (hasNext()) { + char c = next(); + if (escape) { + if (c != '\\' && c != '"') { + throw parseException("invalid escape of '" + c + "'"); + } + escape = false; + } else { + if (c == '\\') { //escape + escape = true; + if (sb != null) { + continue; + } + sb = new StringBuilder(value.substring(oldIndex, index - 1)); + continue; + } + if (c == '"') { + return sb == null ? value.substring(oldIndex, index - 1) : sb.toString(); + } + } + if (sb != null) { + sb.append(c); + } + } + throw parseException("missing end quote"); + } + + public boolean nextArrayElement() { + skipWhitespace(); + if (hasNext() && currentChar() == ',') { + index++; + skipWhitespace(); + return true; + } + return false; + } + + public void expectChar(char c) throws ParseException { + skipWhitespace(); + boolean hasNext = hasNext(); + if (hasNext && currentChar() == c) { + index++; + return; + } + throw parseException("expected '" + c + "' but got " + (hasNext ? "'" + currentChar() + "'" : "EOF")); + } + + public void skipWhitespace() { + while (hasNext() && Character.isWhitespace(currentChar())) { + index++; + } + } + + public boolean hasNext() { + return index < value.length(); + } + + public boolean hasCharsLeft(int num) { + return this.index + num < value.length(); + } + + public char currentChar() { + return value.charAt(index); + } + + public char next() { + return value.charAt(index++); + } + + public void skip(int offset) { + index += offset; + } + + public char lookAhead(int offset) { + return value.charAt(index + offset); + } + + private static boolean isSimpleChar(char c) { + return c >= 'a' && c <= 'z' + || c >= 'A' && c <= 'Z' + || c >= '0' && c <= '9' + || c == '-' + || c == '+' + || c == '.' + || c == '_'; + } + + public ParseException parseException(String msg) { + return new ParseException(msg, value, index); + } +} diff --git a/src/main/java/net/querz/nbt/tag/ArrayTag.java b/src/main/java/net/querz/nbt/tag/ArrayTag.java new file mode 100644 index 000000000..2842fa636 --- /dev/null +++ b/src/main/java/net/querz/nbt/tag/ArrayTag.java @@ -0,0 +1,46 @@ +package net.querz.nbt.tag; + +import java.lang.reflect.Array; + +/** + * ArrayTag is an abstract representation of any NBT array tag. + * For implementations see {@link ByteArrayTag}, {@link IntArrayTag}, {@link LongArrayTag}. + * @param The array type. + * */ +public abstract class ArrayTag extends Tag { + + public ArrayTag(T value) { + super(value); + if (!value.getClass().isArray()) { + throw new UnsupportedOperationException("type of array tag must be an array"); + } + } + + public int length() { + return Array.getLength(getValue()); + } + + @Override + public T getValue() { + return super.getValue(); + } + + @Override + public void setValue(T value) { + super.setValue(value); + } + + @Override + public String valueToString(int maxDepth) { + return arrayToString("", ""); + } + + protected String arrayToString(String prefix, String suffix) { + StringBuilder sb = new StringBuilder("[").append(prefix).append("".equals(prefix) ? "" : ";"); + for (int i = 0; i < length(); i++) { + sb.append(i == 0 ? "" : ",").append(Array.get(getValue(), i)).append(suffix); + } + sb.append("]"); + return sb.toString(); + } +} diff --git a/src/main/java/net/querz/nbt/tag/ByteArrayTag.java b/src/main/java/net/querz/nbt/tag/ByteArrayTag.java new file mode 100644 index 000000000..8fbcf8a30 --- /dev/null +++ b/src/main/java/net/querz/nbt/tag/ByteArrayTag.java @@ -0,0 +1,42 @@ +package net.querz.nbt.tag; + +import java.util.Arrays; + +public class ByteArrayTag extends ArrayTag implements Comparable { + + public static final byte ID = 7; + public static final byte[] ZERO_VALUE = new byte[0]; + + public ByteArrayTag() { + super(ZERO_VALUE); + } + + public ByteArrayTag(byte[] value) { + super(value); + } + + @Override + public byte getID() { + return ID; + } + + @Override + public boolean equals(Object other) { + return super.equals(other) && Arrays.equals(getValue(), ((ByteArrayTag) other).getValue()); + } + + @Override + public int hashCode() { + return Arrays.hashCode(getValue()); + } + + @Override + public int compareTo(ByteArrayTag other) { + return Integer.compare(length(), other.length()); + } + + @Override + public ByteArrayTag clone() { + return new ByteArrayTag(Arrays.copyOf(getValue(), length())); + } +} diff --git a/src/main/java/net/querz/nbt/tag/ByteTag.java b/src/main/java/net/querz/nbt/tag/ByteTag.java new file mode 100644 index 000000000..207cefd25 --- /dev/null +++ b/src/main/java/net/querz/nbt/tag/ByteTag.java @@ -0,0 +1,47 @@ +package net.querz.nbt.tag; + +public class ByteTag extends NumberTag implements Comparable { + + public static final byte ID = 1; + public static final byte ZERO_VALUE = 0; + + public ByteTag() { + super(ZERO_VALUE); + } + + public ByteTag(byte value) { + super(value); + } + + public ByteTag(boolean value) { + super((byte) (value ? 1 : 0)); + } + + @Override + public byte getID() { + return ID; + } + + public boolean asBoolean() { + return getValue() > 0; + } + + public void setValue(byte value) { + super.setValue(value); + } + + @Override + public boolean equals(Object other) { + return super.equals(other) && asByte() == ((ByteTag) other).asByte(); + } + + @Override + public int compareTo(ByteTag other) { + return getValue().compareTo(other.getValue()); + } + + @Override + public ByteTag clone() { + return new ByteTag(getValue()); + } +} diff --git a/src/main/java/net/querz/nbt/tag/CompoundTag.java b/src/main/java/net/querz/nbt/tag/CompoundTag.java new file mode 100644 index 000000000..3909a72c1 --- /dev/null +++ b/src/main/java/net/querz/nbt/tag/CompoundTag.java @@ -0,0 +1,278 @@ +package net.querz.nbt.tag; + +import net.querz.io.MaxDepthIO; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiConsumer; + +public class CompoundTag extends Tag>> implements Iterable>>, Comparable, MaxDepthIO { + + public static final byte ID = 10; + + public CompoundTag() { + super(createEmptyValue()); + } + + @Override + public byte getID() { + return ID; + } + + private static Map> createEmptyValue() { + return new HashMap<>(8); + } + + public int size() { + return getValue().size(); + } + + public Tag remove(String key) { + return getValue().remove(key); + } + + public void clear() { + getValue().clear(); + } + + public boolean containsKey(String key) { + return getValue().containsKey(key); + } + + public boolean containsValue(Tag value) { + return getValue().containsValue(value); + } + + public Collection> values() { + return getValue().values(); + } + + public Set keySet() { + return getValue().keySet(); + } + + public Set>> entrySet() { + return new NonNullEntrySet<>(getValue().entrySet()); + } + + @Override + public Iterator>> iterator() { + return entrySet().iterator(); + } + + public void forEach(BiConsumer> action) { + getValue().forEach(action); + } + + public > C get(String key, Class type) { + Tag t = getValue().get(key); + if (t != null) { + return type.cast(t); + } + return null; + } + + public Tag get(String key) { + return getValue().get(key); + } + + public ByteTag getByteTag(String key) { + return get(key, ByteTag.class); + } + + public ShortTag getShortTag(String key) { + return get(key, ShortTag.class); + } + + public IntTag getIntTag(String key) { + return get(key, IntTag.class); + } + + public LongTag getLongTag(String key) { + return get(key, LongTag.class); + } + + public FloatTag getFloatTag(String key) { + return get(key, FloatTag.class); + } + + public DoubleTag getDoubleTag(String key) { + return get(key, DoubleTag.class); + } + + public StringTag getStringTag(String key) { + return get(key, StringTag.class); + } + + public ByteArrayTag getByteArrayTag(String key) { + return get(key, ByteArrayTag.class); + } + + public IntArrayTag getIntArrayTag(String key) { + return get(key, IntArrayTag.class); + } + + public LongArrayTag getLongArrayTag(String key) { + return get(key, LongArrayTag.class); + } + + public ListTag getListTag(String key) { + return get(key, ListTag.class); + } + + public CompoundTag getCompoundTag(String key) { + return get(key, CompoundTag.class); + } + + public boolean getBoolean(String key) { + Tag t = get(key); + return t instanceof ByteTag && ((ByteTag) t).asByte() > 0; + } + + public byte getByte(String key) { + ByteTag t = getByteTag(key); + return t == null ? ByteTag.ZERO_VALUE : t.asByte(); + } + + public short getShort(String key) { + ShortTag t = getShortTag(key); + return t == null ? ShortTag.ZERO_VALUE : t.asShort(); + } + + public int getInt(String key) { + IntTag t = getIntTag(key); + return t == null ? IntTag.ZERO_VALUE : t.asInt(); + } + + public long getLong(String key) { + LongTag t = getLongTag(key); + return t == null ? LongTag.ZERO_VALUE : t.asLong(); + } + + public float getFloat(String key) { + FloatTag t = getFloatTag(key); + return t == null ? FloatTag.ZERO_VALUE : t.asFloat(); + } + + public double getDouble(String key) { + DoubleTag t = getDoubleTag(key); + return t == null ? DoubleTag.ZERO_VALUE : t.asDouble(); + } + + public String getString(String key) { + StringTag t = getStringTag(key); + return t == null ? StringTag.ZERO_VALUE : t.getValue(); + } + + public byte[] getByteArray(String key) { + ByteArrayTag t = getByteArrayTag(key); + return t == null ? ByteArrayTag.ZERO_VALUE : t.getValue(); + } + + public int[] getIntArray(String key) { + IntArrayTag t = getIntArrayTag(key); + return t == null ? IntArrayTag.ZERO_VALUE : t.getValue(); + } + + public long[] getLongArray(String key) { + LongArrayTag t = getLongArrayTag(key); + return t == null ? LongArrayTag.ZERO_VALUE : t.getValue(); + } + + public Tag put(String key, Tag tag) { + return getValue().put(Objects.requireNonNull(key), Objects.requireNonNull(tag)); + } + + public Tag putBoolean(String key, boolean value) { + return put(key, new ByteTag(value)); + } + + public Tag putByte(String key, byte value) { + return put(key, new ByteTag(value)); + } + + public Tag putShort(String key, short value) { + return put(key, new ShortTag(value)); + } + + public Tag putInt(String key, int value) { + return put(key, new IntTag(value)); + } + + public Tag putLong(String key, long value) { + return put(key, new LongTag(value)); + } + + public Tag putFloat(String key, float value) { + return put(key, new FloatTag(value)); + } + + public Tag putDouble(String key, double value) { + return put(key, new DoubleTag(value)); + } + + public Tag putString(String key, String value) { + return put(key, new StringTag(value)); + } + + public Tag putByteArray(String key, byte[] value) { + return put(key, new ByteArrayTag(value)); + } + + public Tag putIntArray(String key, int[] value) { + return put(key, new IntArrayTag(value)); + } + + public Tag putLongArray(String key, long[] value) { + return put(key, new LongArrayTag(value)); + } + + @Override + public String valueToString(int maxDepth) { + StringBuilder sb = new StringBuilder("{"); + boolean first = true; + for (Map.Entry> e : getValue().entrySet()) { + sb.append(first ? "" : ",") + .append(escapeString(e.getKey(), false)).append(":") + .append(e.getValue().toString(decrementMaxDepth(maxDepth))); + first = false; + } + sb.append("}"); + return sb.toString(); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!super.equals(other) || size() != ((CompoundTag) other).size()) { + return false; + } + for (Map.Entry> e : getValue().entrySet()) { + Tag v; + if ((v = ((CompoundTag) other).get(e.getKey())) == null || !e.getValue().equals(v)) { + return false; + } + } + return true; + } + + @Override + public int compareTo(CompoundTag o) { + return Integer.compare(size(), o.getValue().size()); + } + + @Override + public CompoundTag clone() { + CompoundTag copy = new CompoundTag(); + for (Map.Entry> e : getValue().entrySet()) { + copy.put(e.getKey(), e.getValue().clone()); + } + return copy; + } +} diff --git a/src/main/java/net/querz/nbt/tag/DoubleTag.java b/src/main/java/net/querz/nbt/tag/DoubleTag.java new file mode 100644 index 000000000..28d08658b --- /dev/null +++ b/src/main/java/net/querz/nbt/tag/DoubleTag.java @@ -0,0 +1,39 @@ +package net.querz.nbt.tag; + +public class DoubleTag extends NumberTag implements Comparable { + + public static final byte ID = 6; + public static final double ZERO_VALUE = 0.0D; + + public DoubleTag() { + super(ZERO_VALUE); + } + + public DoubleTag(double value) { + super(value); + } + + @Override + public byte getID() { + return ID; + } + + public void setValue(double value) { + super.setValue(value); + } + + @Override + public boolean equals(Object other) { + return super.equals(other) && getValue().equals(((DoubleTag) other).getValue()); + } + + @Override + public int compareTo(DoubleTag other) { + return getValue().compareTo(other.getValue()); + } + + @Override + public DoubleTag clone() { + return new DoubleTag(getValue()); + } +} diff --git a/src/main/java/net/querz/nbt/tag/EndTag.java b/src/main/java/net/querz/nbt/tag/EndTag.java new file mode 100644 index 000000000..30b970b80 --- /dev/null +++ b/src/main/java/net/querz/nbt/tag/EndTag.java @@ -0,0 +1,31 @@ +package net.querz.nbt.tag; + +public final class EndTag extends Tag { + + public static final byte ID = 0; + public static final EndTag INSTANCE = new EndTag(); + + private EndTag() { + super(null); + } + + @Override + public byte getID() { + return ID; + } + + @Override + protected Void checkValue(Void value) { + return value; + } + + @Override + public String valueToString(int maxDepth) { + return "\"end\""; + } + + @Override + public EndTag clone() { + return INSTANCE; + } +} diff --git a/src/main/java/net/querz/nbt/tag/FloatTag.java b/src/main/java/net/querz/nbt/tag/FloatTag.java new file mode 100644 index 000000000..9d79204fb --- /dev/null +++ b/src/main/java/net/querz/nbt/tag/FloatTag.java @@ -0,0 +1,39 @@ +package net.querz.nbt.tag; + +public class FloatTag extends NumberTag implements Comparable { + + public static final byte ID = 5; + public static final float ZERO_VALUE = 0.0F; + + public FloatTag() { + super(ZERO_VALUE); + } + + public FloatTag(float value) { + super(value); + } + + @Override + public byte getID() { + return ID; + } + + public void setValue(float value) { + super.setValue(value); + } + + @Override + public boolean equals(Object other) { + return super.equals(other) && getValue().equals(((FloatTag) other).getValue()); + } + + @Override + public int compareTo(FloatTag other) { + return getValue().compareTo(other.getValue()); + } + + @Override + public FloatTag clone() { + return new FloatTag(getValue()); + } +} diff --git a/src/main/java/net/querz/nbt/tag/IntArrayTag.java b/src/main/java/net/querz/nbt/tag/IntArrayTag.java new file mode 100644 index 000000000..1799c93c9 --- /dev/null +++ b/src/main/java/net/querz/nbt/tag/IntArrayTag.java @@ -0,0 +1,42 @@ +package net.querz.nbt.tag; + +import java.util.Arrays; + +public class IntArrayTag extends ArrayTag implements Comparable { + + public static final byte ID = 11; + public static final int[] ZERO_VALUE = new int[0]; + + public IntArrayTag() { + super(ZERO_VALUE); + } + + public IntArrayTag(int[] value) { + super(value); + } + + @Override + public byte getID() { + return ID; + } + + @Override + public boolean equals(Object other) { + return super.equals(other) && Arrays.equals(getValue(), ((IntArrayTag) other).getValue()); + } + + @Override + public int hashCode() { + return Arrays.hashCode(getValue()); + } + + @Override + public int compareTo(IntArrayTag other) { + return Integer.compare(length(), other.length()); + } + + @Override + public IntArrayTag clone() { + return new IntArrayTag(Arrays.copyOf(getValue(), length())); + } +} diff --git a/src/main/java/net/querz/nbt/tag/IntTag.java b/src/main/java/net/querz/nbt/tag/IntTag.java new file mode 100644 index 000000000..57c1f2bad --- /dev/null +++ b/src/main/java/net/querz/nbt/tag/IntTag.java @@ -0,0 +1,39 @@ +package net.querz.nbt.tag; + +public class IntTag extends NumberTag implements Comparable { + + public static final byte ID = 3; + public static final int ZERO_VALUE = 0; + + public IntTag() { + super(ZERO_VALUE); + } + + public IntTag(int value) { + super(value); + } + + @Override + public byte getID() { + return ID; + } + + public void setValue(int value) { + super.setValue(value); + } + + @Override + public boolean equals(Object other) { + return super.equals(other) && asInt() == ((IntTag) other).asInt(); + } + + @Override + public int compareTo(IntTag other) { + return getValue().compareTo(other.getValue()); + } + + @Override + public IntTag clone() { + return new IntTag(getValue()); + } +} diff --git a/src/main/java/net/querz/nbt/tag/ListTag.java b/src/main/java/net/querz/nbt/tag/ListTag.java new file mode 100644 index 000000000..008e3aaa3 --- /dev/null +++ b/src/main/java/net/querz/nbt/tag/ListTag.java @@ -0,0 +1,327 @@ +package net.querz.nbt.tag; + +import net.querz.io.MaxDepthIO; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +/** + * 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()}. + * 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. + * */ +public class ListTag> extends Tag> implements Iterable, Comparable>, MaxDepthIO { + + public static final byte ID = 9; + + private Class typeClass = null; + + private ListTag() { + super(createEmptyValue(3)); + } + + @Override + public byte getID() { + return ID; + } + + /** + *

Creates a non-type-safe ListTag. Its element type will be set after the first + * element was added.

+ * + *

This is an internal helper method for cases where the element type is not known + * at construction time. Use {@link #ListTag(Class)} when the type is known.

+ * + * @return A new non-type-safe ListTag + */ + public static ListTag createUnchecked(Class typeClass) { + ListTag list = new ListTag<>(); + list.typeClass = typeClass; + return list; + } + + /** + *

Creates an empty mutable list to be used as empty value of ListTags.

+ * + * @param Type of the list elements + * @param initialCapacity The initial capacity of the returned List + * @return An instance of {@link java.util.List} with an initial capacity of 3 + * */ + private static List createEmptyValue(int initialCapacity) { + return new ArrayList<>(initialCapacity); + } + + /** + * @param typeClass The exact class of the elements + * @throws IllegalArgumentException When {@code typeClass} is {@link EndTag}{@code .class} + * @throws NullPointerException When {@code typeClass} is {@code null} + */ + public ListTag(Class typeClass) throws IllegalArgumentException, NullPointerException { + super(createEmptyValue(3)); + if (typeClass == EndTag.class) { + throw new IllegalArgumentException("cannot create ListTag with EndTag elements"); + } + this.typeClass = Objects.requireNonNull(typeClass); + } + + public Class getTypeClass() { + return typeClass == null ? EndTag.class : typeClass; + } + + public int size() { + return getValue().size(); + } + + public T remove(int index) { + return getValue().remove(index); + } + + public void clear() { + getValue().clear(); + } + + public boolean contains(T t) { + return getValue().contains(t); + } + + public boolean containsAll(Collection> tags) { + return getValue().containsAll(tags); + } + + public void sort(Comparator comparator) { + getValue().sort(comparator); + } + + @Override + public Iterator iterator() { + return getValue().iterator(); + } + + @Override + public void forEach(Consumer action) { + getValue().forEach(action); + } + + public T set(int index, T t) { + return getValue().set(index, Objects.requireNonNull(t)); + } + + /** + * Adds a Tag to this ListTag after the last index. + * @param t The element to be added. + * */ + public void add(T t) { + add(size(), t); + } + + public void add(int index, T t) { + Objects.requireNonNull(t); + if (typeClass == null || typeClass == EndTag.class) { + typeClass = t.getClass(); + } else if (typeClass != t.getClass()) { + throw new ClassCastException( + String.format("cannot add %s to ListTag<%s>", + t.getClass().getSimpleName(), + typeClass.getSimpleName())); + } + getValue().add(index, t); + } + + public void addAll(Collection t) { + for (T tt : t) { + add(tt); + } + } + + public void addAll(int index, Collection t) { + int i = 0; + for (T tt : t) { + add(index + i, tt); + i++; + } + } + + public void addBoolean(boolean value) { + addUnchecked(new ByteTag(value)); + } + + public void addByte(byte value) { + addUnchecked(new ByteTag(value)); + } + + public void addShort(short value) { + addUnchecked(new ShortTag(value)); + } + + public void addInt(int value) { + addUnchecked(new IntTag(value)); + } + + public void addLong(long value) { + addUnchecked(new LongTag(value)); + } + + public void addFloat(float value) { + addUnchecked(new FloatTag(value)); + } + + public void addDouble(double value) { + addUnchecked(new DoubleTag(value)); + } + + public void addString(String value) { + addUnchecked(new StringTag(value)); + } + + public void addByteArray(byte[] value) { + addUnchecked(new ByteArrayTag(value)); + } + + public void addIntArray(int[] value) { + addUnchecked(new IntArrayTag(value)); + } + + public void addLongArray(long[] value) { + addUnchecked(new LongArrayTag(value)); + } + + public T get(int index) { + return getValue().get(index); + } + + public int indexOf(T t) { + return getValue().indexOf(t); + } + + @SuppressWarnings("unchecked") + public > ListTag asTypedList(Class type) { + checkTypeClass(type); + typeClass = type; + return (ListTag) this; + } + + public ListTag asByteTagList() { + return asTypedList(ByteTag.class); + } + + public ListTag asShortTagList() { + return asTypedList(ShortTag.class); + } + + public ListTag asIntTagList() { + return asTypedList(IntTag.class); + } + + public ListTag asLongTagList() { + return asTypedList(LongTag.class); + } + + public ListTag asFloatTagList() { + return asTypedList(FloatTag.class); + } + + public ListTag asDoubleTagList() { + return asTypedList(DoubleTag.class); + } + + public ListTag asStringTagList() { + return asTypedList(StringTag.class); + } + + public ListTag asByteArrayTagList() { + return asTypedList(ByteArrayTag.class); + } + + public ListTag asIntArrayTagList() { + return asTypedList(IntArrayTag.class); + } + + public ListTag asLongArrayTagList() { + return asTypedList(LongArrayTag.class); + } + + @SuppressWarnings("unchecked") + public ListTag> asListTagList() { + checkTypeClass(ListTag.class); + typeClass = ListTag.class; + return (ListTag>) this; + } + + public ListTag asCompoundTagList() { + return asTypedList(CompoundTag.class); + } + + @Override + public String valueToString(int maxDepth) { + StringBuilder sb = new StringBuilder("{\"type\":\"").append(getTypeClass().getSimpleName()).append("\",\"list\":["); + for (int i = 0; i < size(); i++) { + sb.append(i > 0 ? "," : "").append(get(i).valueToString(decrementMaxDepth(maxDepth))); + } + sb.append("]}"); + return sb.toString(); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!super.equals(other) || size() != ((ListTag) other).size() || getTypeClass() != ((ListTag) other).getTypeClass()) { + return false; + } + for (int i = 0; i < size(); i++) { + if (!get(i).equals(((ListTag) other).get(i))) { + return false; + } + } + return true; + } + + @Override + public int hashCode() { + return Objects.hash(getTypeClass().hashCode(), getValue().hashCode()); + } + + @Override + public int compareTo(ListTag o) { + return Integer.compare(size(), o.getValue().size()); + } + + @SuppressWarnings("unchecked") + @Override + public ListTag clone() { + ListTag copy = new ListTag<>(); + // assure type safety for clone + copy.typeClass = typeClass; + for (T t : getValue()) { + copy.add((T) t.clone()); + } + return copy; + } + + //TODO: make private + @SuppressWarnings("unchecked") + public void addUnchecked(Tag tag) { + if (typeClass != null && typeClass != tag.getClass() && typeClass != EndTag.class) { + throw new IllegalArgumentException(String.format( + "cannot add %s to ListTag<%s>", + tag.getClass().getSimpleName(), typeClass.getSimpleName())); + } + add(size(), (T) tag); + } + + private void checkTypeClass(Class clazz) { + if (typeClass != null && typeClass != EndTag.class && typeClass != clazz) { + throw new ClassCastException(String.format( + "cannot cast ListTag<%s> to ListTag<%s>", + typeClass.getSimpleName(), clazz.getSimpleName())); + } + } +} diff --git a/src/main/java/net/querz/nbt/tag/LongArrayTag.java b/src/main/java/net/querz/nbt/tag/LongArrayTag.java new file mode 100644 index 000000000..e0528dd14 --- /dev/null +++ b/src/main/java/net/querz/nbt/tag/LongArrayTag.java @@ -0,0 +1,42 @@ +package net.querz.nbt.tag; + +import java.util.Arrays; + +public class LongArrayTag extends ArrayTag implements Comparable { + + public static final byte ID = 12; + public static final long[] ZERO_VALUE = new long[0]; + + public LongArrayTag() { + super(ZERO_VALUE); + } + + public LongArrayTag(long[] value) { + super(value); + } + + @Override + public byte getID() { + return ID; + } + + @Override + public boolean equals(Object other) { + return super.equals(other) && Arrays.equals(getValue(), ((LongArrayTag) other).getValue()); + } + + @Override + public int hashCode() { + return Arrays.hashCode(getValue()); + } + + @Override + public int compareTo(LongArrayTag other) { + return Integer.compare(length(), other.length()); + } + + @Override + public LongArrayTag clone() { + return new LongArrayTag(Arrays.copyOf(getValue(), length())); + } +} diff --git a/src/main/java/net/querz/nbt/tag/LongTag.java b/src/main/java/net/querz/nbt/tag/LongTag.java new file mode 100644 index 000000000..8f40a3258 --- /dev/null +++ b/src/main/java/net/querz/nbt/tag/LongTag.java @@ -0,0 +1,39 @@ +package net.querz.nbt.tag; + +public class LongTag extends NumberTag implements Comparable { + + public static final byte ID = 4; + public static final long ZERO_VALUE = 0L; + + public LongTag() { + super(ZERO_VALUE); + } + + public LongTag(long value) { + super(value); + } + + @Override + public byte getID() { + return ID; + } + + public void setValue(long value) { + super.setValue(value); + } + + @Override + public boolean equals(Object other) { + return super.equals(other) && asLong() == ((LongTag) other).asLong(); + } + + @Override + public int compareTo(LongTag other) { + return getValue().compareTo(other.getValue()); + } + + @Override + public LongTag clone() { + return new LongTag(getValue()); + } +} diff --git a/src/main/java/net/querz/nbt/tag/NonNullEntrySet.java b/src/main/java/net/querz/nbt/tag/NonNullEntrySet.java new file mode 100644 index 000000000..e157ba264 --- /dev/null +++ b/src/main/java/net/querz/nbt/tag/NonNullEntrySet.java @@ -0,0 +1,140 @@ +package net.querz.nbt.tag; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + * A decorator for the Set returned by CompoundTag#entrySet() + * that disallows setting null values. + * */ +class NonNullEntrySet implements Set> { + + private Set> set; + + NonNullEntrySet(Set> set) { + this.set = set; + } + + @Override + public int size() { + return set.size(); + } + + @Override + public boolean isEmpty() { + return set.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return set.contains(o); + } + + @Override + public Iterator> iterator() { + return new NonNullEntrySetIterator(set.iterator()); + } + + @Override + public Object[] toArray() { + return set.toArray(); + } + + @Override + public T[] toArray(T[] a) { + return set.toArray(a); + } + + @Override + public boolean add(Map.Entry kvEntry) { + return set.add(kvEntry); + } + + @Override + public boolean remove(Object o) { + return set.remove(o); + } + + @Override + public boolean containsAll(Collection c) { + return set.containsAll(c); + } + + @Override + public boolean addAll(Collection> c) { + return set.addAll(c); + } + + @Override + public boolean retainAll(Collection c) { + return set.retainAll(c); + } + + @Override + public boolean removeAll(Collection c) { + return set.removeAll(c); + } + + @Override + public void clear() { + set.clear(); + } + + class NonNullEntrySetIterator implements Iterator> { + + private Iterator> iterator; + + NonNullEntrySetIterator(Iterator> iterator) { + this.iterator = iterator; + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public Map.Entry next() { + return new NonNullEntry(iterator.next()); + } + } + + class NonNullEntry implements Map.Entry { + + private Map.Entry entry; + + NonNullEntry(Map.Entry entry) { + this.entry = entry; + } + + @Override + public K getKey() { + return entry.getKey(); + } + + @Override + public V getValue() { + return entry.getValue(); + } + + @Override + public V setValue(V value) { + if (value == null) { + throw new NullPointerException(getClass().getSimpleName() + " does not allow setting null"); + } + return entry.setValue(value); + } + + @Override + public boolean equals(Object o) { + return entry.equals(o); + } + + @Override + public int hashCode() { + return entry.hashCode(); + } + } +} \ No newline at end of file diff --git a/src/main/java/net/querz/nbt/tag/NumberTag.java b/src/main/java/net/querz/nbt/tag/NumberTag.java new file mode 100644 index 000000000..48faa0248 --- /dev/null +++ b/src/main/java/net/querz/nbt/tag/NumberTag.java @@ -0,0 +1,37 @@ +package net.querz.nbt.tag; + +public abstract class NumberTag> extends Tag { + + public NumberTag(T value) { + super(value); + } + + public byte asByte() { + return getValue().byteValue(); + } + + public short asShort() { + return getValue().shortValue(); + } + + public int asInt() { + return getValue().intValue(); + } + + public long asLong() { + return getValue().longValue(); + } + + public float asFloat() { + return getValue().floatValue(); + } + + public double asDouble() { + return getValue().doubleValue(); + } + + @Override + public String valueToString(int maxDepth) { + return getValue().toString(); + } +} diff --git a/src/main/java/net/querz/nbt/tag/ShortTag.java b/src/main/java/net/querz/nbt/tag/ShortTag.java new file mode 100644 index 000000000..5f434c374 --- /dev/null +++ b/src/main/java/net/querz/nbt/tag/ShortTag.java @@ -0,0 +1,39 @@ +package net.querz.nbt.tag; + +public class ShortTag extends NumberTag implements Comparable { + + public static final byte ID = 2; + public static final short ZERO_VALUE = 0; + + public ShortTag() { + super(ZERO_VALUE); + } + + public ShortTag(short value) { + super(value); + } + + @Override + public byte getID() { + return ID; + } + + public void setValue(short value) { + super.setValue(value); + } + + @Override + public boolean equals(Object other) { + return super.equals(other) && asShort() == ((ShortTag) other).asShort(); + } + + @Override + public int compareTo(ShortTag other) { + return getValue().compareTo(other.getValue()); + } + + @Override + public ShortTag clone() { + return new ShortTag(getValue()); + } +} diff --git a/src/main/java/net/querz/nbt/tag/StringTag.java b/src/main/java/net/querz/nbt/tag/StringTag.java new file mode 100644 index 000000000..0d30c4b62 --- /dev/null +++ b/src/main/java/net/querz/nbt/tag/StringTag.java @@ -0,0 +1,50 @@ +package net.querz.nbt.tag; + +public class StringTag extends Tag implements Comparable { + + public static final byte ID = 8; + public static final String ZERO_VALUE = ""; + + public StringTag() { + super(ZERO_VALUE); + } + + public StringTag(String value) { + super(value); + } + + @Override + public byte getID() { + return ID; + } + + @Override + public String getValue() { + return super.getValue(); + } + + @Override + public void setValue(String value) { + super.setValue(value); + } + + @Override + public String valueToString(int maxDepth) { + return escapeString(getValue(), false); + } + + @Override + public boolean equals(Object other) { + return super.equals(other) && getValue().equals(((StringTag) other).getValue()); + } + + @Override + public int compareTo(StringTag o) { + return getValue().compareTo(o.getValue()); + } + + @Override + public StringTag clone() { + return new StringTag(getValue()); + } +} diff --git a/src/main/java/net/querz/nbt/tag/Tag.java b/src/main/java/net/querz/nbt/tag/Tag.java new file mode 100644 index 000000000..dd1c8d551 --- /dev/null +++ b/src/main/java/net/querz/nbt/tag/Tag.java @@ -0,0 +1,187 @@ +package net.querz.nbt.tag; + +import net.querz.io.MaxDepthReachedException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Base class for all NBT tags. + * + *

Nesting

+ *

All methods serializing instances or deserializing data track the nesting levels to prevent + * circular references or malicious data which could, when deserialized, result in thousands + * of instances causing a denial of service.

+ * + *

These methods have a parameter for the maximum nesting depth they are allowed to traverse. A + * value of {@code 0} means that only the object itself, but no nested objects may be processed. + * If an instance is nested further than allowed, a {@link MaxDepthReachedException} will be thrown. + * Providing a negative maximum nesting depth will cause an {@code IllegalArgumentException} + * to be thrown.

+ * + *

Some methods do not provide a parameter to specify the maximum nesting depth, but instead use + * {@link #DEFAULT_MAX_DEPTH}, which is also the maximum used by Minecraft. This is documented for + * the respective methods.

+ * + *

If custom NBT tags contain objects other than NBT tags, which can be nested as well, then there + * is no guarantee that {@code MaxDepthReachedException}s are thrown for them. The respective class + * will document this behavior accordingly.

+ * + * @param The type of the contained value + * */ +public abstract class Tag implements Cloneable { + + /** + * The default maximum depth of the NBT structure. + * */ + public static final int DEFAULT_MAX_DEPTH = 512; + + private static final Map ESCAPE_CHARACTERS; + static { + final Map temp = new HashMap<>(); + temp.put("\\", "\\\\\\\\"); + temp.put("\n", "\\\\n"); + temp.put("\t", "\\\\t"); + temp.put("\r", "\\\\r"); + temp.put("\"", "\\\\\""); + ESCAPE_CHARACTERS = Collections.unmodifiableMap(temp); + } + + private static final Pattern ESCAPE_PATTERN = Pattern.compile("[\\\\\n\t\r\"]"); + private static final Pattern NON_QUOTE_PATTERN = Pattern.compile("[a-zA-Z0-9_\\-+]+"); + + private T value; + + /** + * Initializes this Tag with some value. If the value is {@code null}, it will + * throw a {@code NullPointerException} + * @param value The value to be set for this Tag. + * */ + public Tag(T value) { + setValue(value); + } + + /** + * @return This Tag's ID, usually used for serialization and deserialization. + * */ + public abstract byte getID(); + + /** + * @return The value of this Tag. + * */ + protected T getValue() { + return value; + } + + /** + * Sets the value for this Tag directly. + * @param value The value to be set. + * @throws NullPointerException If the value is null + * */ + protected void setValue(T value) { + this.value = checkValue(value); + } + + /** + * Checks if the value {@code value} is {@code null}. + * @param value The value to check + * @throws NullPointerException If {@code value} was {@code null} + * @return The parameter {@code value} + * */ + protected T checkValue(T value) { + return Objects.requireNonNull(value); + } + + /** + * Calls {@link Tag#toString(int)} with an initial depth of {@code 0}. + * @see Tag#toString(int) + * @throws MaxDepthReachedException If the maximum nesting depth is exceeded. + * */ + @Override + public final String toString() { + return toString(DEFAULT_MAX_DEPTH); + } + + /** + * Creates a string representation of this Tag in a valid JSON format. + * @param maxDepth The maximum nesting depth. + * @return The string representation of this Tag. + * @throws MaxDepthReachedException If the maximum nesting depth is exceeded. + * */ + public String toString(int maxDepth) { + return "{\"type\":\""+ getClass().getSimpleName() + "\"," + + "\"value\":" + valueToString(maxDepth) + "}"; + } + + /** + * Calls {@link Tag#valueToString(int)} with {@link Tag#DEFAULT_MAX_DEPTH}. + * @return The string representation of the value of this Tag. + * @throws MaxDepthReachedException If the maximum nesting depth is exceeded. + * */ + public String valueToString() { + return valueToString(DEFAULT_MAX_DEPTH); + } + + /** + * Returns a JSON representation of the value of this Tag. + * @param maxDepth The maximum nesting depth. + * @return The string representation of the value of this Tag. + * @throws MaxDepthReachedException If the maximum nesting depth is exceeded. + * */ + public abstract String valueToString(int maxDepth); + + /** + * Returns whether this Tag and some other Tag are equal. + * They are equal if {@code other} is not {@code null} and they are of the same class. + * Custom Tag implementations should overwrite this but check the result + * of this {@code super}-method while comparing. + * @param other The Tag to compare to. + * @return {@code true} if they are equal based on the conditions mentioned above. + * */ + @Override + public boolean equals(Object other) { + return other != null && getClass() == other.getClass(); + } + + /** + * Calculates the hash code of this Tag. Tags which are equal according to {@link Tag#equals(Object)} + * must return an equal hash code. + * @return The hash code of this Tag. + * */ + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * Creates a clone of this Tag. + * @return A clone of this Tag. + * */ + @SuppressWarnings("CloneDoesntDeclareCloneNotSupportedException") + public abstract Tag clone(); + + /** + * Escapes a string to fit into a JSON-like string representation for Minecraft + * or to create the JSON string representation of a Tag returned from {@link Tag#toString()} + * @param s The string to be escaped. + * @param lenient {@code true} if it should force double quotes ({@code "}) at the start and + * the end of the string. + * @return The escaped string. + * */ + protected static String escapeString(String s, boolean lenient) { + StringBuffer sb = new StringBuffer(); + Matcher m = ESCAPE_PATTERN.matcher(s); + while (m.find()) { + m.appendReplacement(sb, ESCAPE_CHARACTERS.get(m.group())); + } + m.appendTail(sb); + m = NON_QUOTE_PATTERN.matcher(s); + if (!lenient || !m.matches()) { + sb.insert(0, "\"").append("\""); + } + return sb.toString(); + } +}