diff --git a/.gitignore b/.gitignore index ad25607b..7a2ac475 100644 --- a/.gitignore +++ b/.gitignore @@ -77,11 +77,13 @@ local.properties # IDEA folder .idea/ +# .iml files +*.iml + # ignore javadoc for now doc/ # ignore out out/ -# ignore patches -*.patch \ No newline at end of file +*.patch diff --git a/LICENSE b/LICENSE index a786dfd1..3b203f48 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2016 Querz +Copyright (c) 2016 - 2020 Querz Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 004c51a2..b629063b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # NBT [![Build Status](https://travis-ci.org/Querz/NBT.svg?branch=master)](https://travis-ci.org/Querz/NBT) [![Coverage Status](https://img.shields.io/coveralls/github/Querz/NBT/master.svg)](https://coveralls.io/github/Querz/NBT?branch=master) [![Release](https://jitpack.io/v/Querz/NBT.svg)](https://jitpack.io/#Querz/NBT) -#### A java implementation of the [NBT protocol](http://minecraft.gamepedia.com/NBT_format), including a way to implement custom tags. +#### A java implementation of the [NBT protocol](http://minecraft.gamepedia.com/NBT_format) for Minecraft Java Edition. --- ### Specification According to the [specification](https://minecraft.gamepedia.com/NBT_format), there are currently 13 different types of tags: @@ -25,6 +25,41 @@ According to the [specification](https://minecraft.gamepedia.com/NBT_format), th * The maximum depth of the NBT structure is 512. If the depth exceeds this restriction during serialization, deserialization or String conversion, a `MaxDepthReachedException` is thrown. This usually happens when a circular reference exists in the NBT structure. The NBT specification does not allow circular references, as there is no tag to represent this. +### Add the library as a dependency using Gradle: +Add Jitpack to your `repositories`: +``` +repositories { + ... + maven { url 'https://jitpack.io/' } +} +``` +And then add it as a dependency as usual: +``` +dependencies { + ... + implementation 'com.github.Querz:NBT:6.1' +} +``` + +### Add the library as a dependency using Maven: +Add Jitpack: +``` + + + jitpack.io + https://jitpack.io + + +``` +Dependency: +``` + + com.github.Querz + NBT + 6.1 + +``` + --- ### Example usage: The following code snippet shows how to create a `CompoundTag`: @@ -56,33 +91,31 @@ Some methods do not provide a parameter to specify the maximum depth, but instea ### Utility There are several utility methods to make your life easier if you use this library. #### NBTUtil -`NBTUtil.writeTag()` lets you write a Tag into a gzip compressed or uncompressed file in one line (not counting exception handling). Files are gzip compressed by default. +`NBTUtil.write()` lets you write a Tag into a gzip compressed or uncompressed file in one line (not counting exception handling). Files are gzip compressed by default. Example usage: ```java -NBTUtil.writeTag(tag, "filename.dat"); +NBTUtil.write(namedTag, "filename.dat"); ``` -`NBTUtil.readTag()` reads any file containing NBT data. No worry about compression, it will automatically uncompress gzip compressed files. +`NBTUtil.read()` reads any file containing NBT data. No worry about compression, it will automatically uncompress gzip compressed files. Example usage: ```java -Tag tag = NBTUtil.readTag("filename.dat"); +NamedTag namedTag = NBTUtil.read("filename.dat"); ``` #### Playing Minecraft? -Each tag can be converted into a JSON-like NBT String used in Minecraft commands. +Each tag can be converted into an NBT String (SNBT) used in Minecraft commands. Example usage: ```java CompoundTag c = new CompoundTag(); c.putByte("blah", (byte) 5); c.putString("foo", "bär"); -System.out.println(c.toTagString()); // {blah:5b,foo:"bär"} - ListTag s = new ListTag<>(StringTag.class); s.addString("test"); s.add(new StringTag("text")); c.add("list", s); -System.out.println(c.toTagString()); // {blah:5b,foo:"bär",list:[test,text]} +System.out.println(SNBTUtil.toSNBT(c)); // {blah:5b,foo:"bär",list:[test,text]} ``` There is also a tool to read, change and write MCA files. @@ -117,25 +150,3 @@ mcaFile.cleanupPalettesAndBlockStates(); chunk.cleanupPalettesAndBlockStates(); section.cleanupPaletteAndBlockStates(); ``` - ---- -### Custom tags -Interested in more advanced features, and the default NBT protocol just isn't enough? Simply create your own tags! -There are 4 example classes in `net.querz.nbt.custom` that show how to implement custom tags: - -| Class | ID | Description | -| ------------- | :-: | ----------- | -| [ObjectTag](src/main/java/net/querz/nbt/custom/ObjectTag.java) | 90 | A wrapper tag that serializes and deserializes any object using the default java serialization. | -| [ShortArrayTag](src/main/java/net/querz/nbt/custom/ShortArrayTag.java) | 100 | In addition to the already existing `ByteArrayTag`, `IntArrayTag` and `LongArrayTag`. | -| [CharTag](src/main/java/net/querz/nbt/custom/CharTag.java) | 110 | `Character` (char) tag. | -| [StructTag](src/main/java/net/querz/nbt/custom/StructTag.java) | 120 | Similar to the `ListTag`, but with the ability to store multiple types. | - -To be able to use a custom tag with deserialization, a `Supplier` and the custom tag class must be registered at runtime alongside its id with `TagFactory.registerCustomTag()`. The `Supplier` can be anything that returns a new instance of this custom tag. Here is an example using the custom tags no-args constructor: -```java -TagFactory.registerCustomTag(90, ObjectTag::new, ObjectTag.class); -``` - -#### Nesting -As mentioned before, serialization and deserialization methods are provided with a parameter indicating the maximum processing depth of the structure. This is not guaranteed when using custom tags, it is the responsibility of the creator of that custom tag to call `Tag#decrementMaxDepth(int)` to correctly update the nesting depth. - -It is also highly encouraged to document the custom tag behaviour when it does so to make users aware of the possible exceptions thrown by `Tag#decrementMaxDepth(int)`. \ No newline at end of file diff --git a/build.gradle b/build.gradle index c992faa4..c3c1eaf9 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,6 @@ plugins { id 'com.github.kt3k.coveralls' version '2.4.0' + id 'maven' } apply plugin: 'java' @@ -9,12 +10,13 @@ apply plugin: 'jacoco' group = 'net.querz.nbt' archivesBaseName = 'nbt' -version = '4.1' +version = '6.1-SNAPSHOT' sourceCompatibility = '1.8' targetCompatibility = '1.8' compileJava.options.encoding = 'UTF-8' repositories { + mavenLocal() jcenter() } @@ -53,9 +55,3 @@ artifacts { archives sourcesJar archives javadocJar } - -jar { - manifest { - attributes('Automatic-Module-Name': 'net.querz.nbt') - } -} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index ae6575fa..62d4c053 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 77a041f6..622ab64a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Wed Mar 29 16:28:10 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip diff --git a/gradlew b/gradlew index 4453ccea..fbd7c515 100755 --- a/gradlew +++ b/gradlew @@ -1,5 +1,21 @@ #!/usr/bin/env sh +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + ############################################################################## ## ## Gradle start up script for UN*X @@ -28,16 +44,16 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -66,6 +82,7 @@ esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then @@ -109,10 +126,11 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath @@ -138,35 +156,30 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args -save ( ) { +save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=$(save "$@") +APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index e95643d6..a9f778a7 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -13,8 +29,11 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -65,6 +84,7 @@ set CMD_LINE_ARGS=%* set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 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 00000000..1849fe9d --- /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 00000000..c34dba72 --- /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 00000000..d49ccc90 --- /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 00000000..0a5fc3e7 --- /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/nbt/MaxDepthReachedException.java b/src/main/java/net/querz/io/MaxDepthReachedException.java similarity index 91% rename from src/main/java/net/querz/nbt/MaxDepthReachedException.java rename to src/main/java/net/querz/io/MaxDepthReachedException.java index 3b3dfb71..eb903228 100644 --- a/src/main/java/net/querz/nbt/MaxDepthReachedException.java +++ b/src/main/java/net/querz/io/MaxDepthReachedException.java @@ -1,10 +1,11 @@ -package net.querz.nbt; +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 00000000..a6c9377a --- /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 00000000..2160e2a8 --- /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 00000000..c4da8104 --- /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/nbt/mca/Chunk.java b/src/main/java/net/querz/mca/Chunk.java similarity index 54% rename from src/main/java/net/querz/nbt/mca/Chunk.java rename to src/main/java/net/querz/mca/Chunk.java index 5ab0fe51..dd84e1fe 100644 --- a/src/main/java/net/querz/nbt/mca/Chunk.java +++ b/src/main/java/net/querz/mca/Chunk.java @@ -1,20 +1,29 @@ -package net.querz.nbt.mca; +package net.querz.mca; -import net.querz.nbt.CompoundTag; -import net.querz.nbt.ListTag; -import net.querz.nbt.Tag; +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.DataInputStream; -import java.io.DataOutputStream; import java.io.FileInputStream; import java.io.IOException; import java.io.RandomAccessFile; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Map; +import java.util.TreeMap; -public class Chunk { +import static net.querz.mca.LoadFlags.*; - public static final int DEFAULT_DATA_VERSION = 1628; +public class Chunk implements Iterable
{ + + public static final int DEFAULT_DATA_VERSION = 2567; + + private boolean partial; + private boolean raw; private int lastMCAUpdate; @@ -26,7 +35,7 @@ public class Chunk { private int[] biomes; private CompoundTag heightMaps; private CompoundTag carvingMasks; - private Section[] sections = new Section[16]; //always initialized with size = 16 for fast access + private Map sections = new TreeMap<>(); private ListTag entities; private ListTag tileEntities; private ListTag tileTicks; @@ -48,46 +57,72 @@ public class Chunk { */ public Chunk(CompoundTag data) { this.data = data; - initReferences(); + initReferences(ALL_DATA); } - private void initReferences() { + 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"); - } - this.dataVersion = data.getInt("DataVersion"); - this.inhabitedTime = level.getLong("InhabitedTime"); - this.lastUpdate = level.getLong("LastUpdate"); - this.biomes = level.getIntArray("Biomes"); - this.heightMaps = level.getCompoundTag("HeightMaps"); - this.carvingMasks = level.getCompoundTag("CarvingMasks"); - this.entities = level.containsKey("Entities") ? level.getListTag("Entities").asCompoundTagList() : null; - this.tileEntities = level.containsKey("TileEntities") ? level.getListTag("TileEntities").asCompoundTagList() : null; - this.tileTicks = level.containsKey("TileTicks") ? level.getListTag("TileTicks").asCompoundTagList() : null; - this.liquidTicks = level.containsKey("LiquidTicks") ? level.getListTag("LiquidTicks").asCompoundTagList() : null; - this.lights = level.containsKey("Lights") ? level.getListTag("Lights").asListTagList() : null; - this.liquidsToBeTicked = level.containsKey("LiquidsToBeTicked") ? level.getListTag("LiquidsToBeTicked").asListTagList() : null; - this.toBeTicked = level.containsKey("ToBeTicked") ? level.getListTag("ToBeTicked").asListTagList() : null; - this.postProcessing = level.containsKey("PostProcessing") ? level.getListTag("PostProcessing").asListTagList() : null; - this.status = level.getString("Status"); - this.structures = level.getCompoundTag("Structures"); - if (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); - if (newSection.isEmpty()) { - continue; - } - this.sections[sectionIndex] = newSection; + + if ((loadFlags != ALL_DATA) && (loadFlags & RAW) != 0) { + raw = true; + return; + } + + dataVersion = data.getInt("DataVersion"); + inhabitedTime = data.getLong("InhabitedTime"); + lastUpdate = data.getLong("LastUpdate"); + if ((loadFlags & BIOMES) != 0) { + biomes = data.getIntArray("Biomes"); + } + if ((loadFlags & HEIGHTMAPS) != 0) { + heightMaps = data.getCompoundTag("Heightmaps"); + } + if ((loadFlags & CARVING_MASKS) != 0) { + carvingMasks = data.getCompoundTag("CarvingMasks"); + } + if ((loadFlags & ENTITIES) != 0) { + entities = data.containsKey("Entities") ? data.getListTag("Entities").asCompoundTagList() : null; + } + if ((loadFlags & TILE_ENTITIES) != 0) { + tileEntities = data.containsKey("TileEntities") ? data.getListTag("TileEntities").asCompoundTagList() : null; + } + if ((loadFlags & TILE_TICKS) != 0) { + tileTicks = data.containsKey("TileTicks") ? data.getListTag("TileTicks").asCompoundTagList() : null; + } + if ((loadFlags & LIQUID_TICKS) != 0) { + liquidTicks = data.containsKey("LiquidTicks") ? data.getListTag("LiquidTicks").asCompoundTagList() : null; + } + if ((loadFlags & LIGHTS) != 0) { + lights = data.containsKey("Lights") ? data.getListTag("Lights").asListTagList() : null; + } + if ((loadFlags & LIQUIDS_TO_BE_TICKED) != 0) { + liquidsToBeTicked = data.containsKey("LiquidsToBeTicked") ? data.getListTag("LiquidsToBeTicked").asListTagList() : null; + } + if ((loadFlags & TO_BE_TICKED) != 0) { + toBeTicked = data.containsKey("ToBeTicked") ? data.getListTag("ToBeTicked").asListTagList() : null; + } + if ((loadFlags & POST_PROCESSING) != 0) { + postProcessing = data.containsKey("PostProcessing") ? data.getListTag("PostProcessing").asListTagList() : null; + } + status = data.getString("Status"); + if ((loadFlags & STRUCTURES) != 0) { + structures = data.getCompoundTag("Structures"); + } + if ((loadFlags & (BLOCK_LIGHTS|BLOCK_STATES|SKY_LIGHT)) != 0 && data.containsKey("Sections")) { + for (CompoundTag section : data.getListTag("Sections").asCompoundTagList()) { + int sectionIndex = section.getNumber("Y").byteValue(); + Section newSection = new Section(section, dataVersion, loadFlags); + sections.put(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; + } } /** @@ -96,15 +131,19 @@ private void initReferences() { * @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 (DataOutputStream nbtOut = new DataOutputStream(new BufferedOutputStream(CompressionType.ZLIB.compress(baos)))) { - updateHandle(xPos, zPos).serialize(nbtOut, Tag.DEFAULT_MAX_DEPTH); + 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); + 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; @@ -116,63 +155,131 @@ public int serialize(RandomAccessFile raf, int xPos, int zPos) throws IOExceptio * @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); } - DataInputStream dis = new DataInputStream(new BufferedInputStream(compressionType.decompress(new FileInputStream(raf.getFD())))); - Tag tag = Tag.deserialize(dis, Tag.DEFAULT_MAX_DEPTH); - if (tag instanceof CompoundTag) { - data = (CompoundTag) tag; - initReferences(); + 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())); } } /** - * Fetches a biome id at a specific block column in this chunk. - * 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. - * @return The biome id or -1 if the biomes are not correctly initialized. + * @deprecated Use {@link #getBiomeAt(int, int, int)} instead */ + @Deprecated public int getBiomeAt(int blockX, int blockZ) { - if (biomes == null || biomes.length != 256) { - return -1; + 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"); } - return biomes[getBlockIndex(blockX, blockZ)]; } /** - * Sets a biome id at a specific block column. + * 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 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. + * @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 (biomes == null || biomes.length != 256) { - biomes = new int[256]; - for (int i = 0; i < biomes.length; i++) { - biomes[i] = -1; + checkRaw(); + 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; } } - biomes[getBlockIndex(blockX, blockZ)] = biomeID; } - /** - * Fetches the block state at a specific block location in this chunk. - * The block coordinate 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. - * @return The block state or null if a section at the location does not exist. - */ + /** + * 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) { + checkRaw(); + 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 * 16 + biomeZ * 4 + biomeX; + } + public CompoundTag getBlockStateAt(int blockX, int blockY, int blockZ) { - Section section = sections[MCAUtil.blockToChunk(blockY)]; + Section section = sections.get(MCAUtil.blockToChunk(blockY)); if (section == null) { return null; } @@ -191,10 +298,11 @@ public CompoundTag getBlockStateAt(int blockX, int blockY, int blockZ) { * Recalculating the Palette should only be executed once right before saving the Chunk to file. */ public void setBlockStateAt(int blockX, int blockY, int blockZ, CompoundTag state, boolean cleanup) { + checkRaw(); int sectionIndex = MCAUtil.blockToChunk(blockY); - Section section = sections[sectionIndex]; + Section section = sections.get(sectionIndex); if (section == null) { - section = sections[sectionIndex] = Section.newSection(); + sections.put(sectionIndex, section = Section.newSection()); } section.setBlockStateAt(blockX, blockY, blockZ, state, cleanup); } @@ -212,7 +320,13 @@ public int getDataVersion() { * @param dataVersion The DataVersion to be set. */ public void setDataVersion(int dataVersion) { + checkRaw(); this.dataVersion = dataVersion; + for (Section section : sections.values()) { + if (section != null) { + section.dataVersion = dataVersion; + } + } } /** @@ -227,6 +341,7 @@ public int getLastMCAUpdate() { * @param lastMCAUpdate The time in seconds since 1970-01-01. */ public void setLastMCAUpdate(int lastMCAUpdate) { + checkRaw(); this.lastMCAUpdate = lastMCAUpdate; } @@ -242,6 +357,7 @@ public String getStatus() { * @param status The generation status of this chunk. */ public void setStatus(String status) { + checkRaw(); this.status = status; } @@ -251,7 +367,7 @@ public void setStatus(String status) { * @return The Section. */ public Section getSection(int sectionY) { - return sections[sectionY]; + return sections.get(sectionY); } /** @@ -260,7 +376,8 @@ public Section getSection(int sectionY) { * @param section The section to be set. */ public void setSection(int sectionY, Section section) { - sections[sectionY] = section; + checkRaw(); + sections.put(sectionY, section); } /** @@ -275,6 +392,7 @@ public long getLastUpdate() { * @param lastUpdate The UNIX timestamp. */ public void setLastUpdate(long lastUpdate) { + checkRaw(); this.lastUpdate = lastUpdate; } @@ -290,6 +408,7 @@ public long getInhabitedTime() { * @param inhabitedTime The time in ticks. */ public void setInhabitedTime(long inhabitedTime) { + checkRaw(); this.inhabitedTime = inhabitedTime; } @@ -307,8 +426,11 @@ public int[] getBiomes() { * or is null */ public void setBiomes(int[] biomes) { - if (biomes != null && biomes.length != 256) { - throw new IllegalArgumentException("biomes array must have a length of 256"); + checkRaw(); + if (biomes != null) { + if (dataVersion < 2202 && biomes.length != 256 || dataVersion >= 2202 && biomes.length != 1024) { + throw new IllegalArgumentException("biomes array must have a length of " + (dataVersion < 2202 ? "256" : "1024")); + } } this.biomes = biomes; } @@ -325,6 +447,7 @@ public CompoundTag getHeightMaps() { * @param heightMaps The height maps. */ public void setHeightMaps(CompoundTag heightMaps) { + checkRaw(); this.heightMaps = heightMaps; } @@ -340,6 +463,7 @@ public CompoundTag getCarvingMasks() { * @param carvingMasks The carving masks. */ public void setCarvingMasks(CompoundTag carvingMasks) { + checkRaw(); this.carvingMasks = carvingMasks; } @@ -355,6 +479,7 @@ public ListTag getEntities() { * @param entities The entities. */ public void setEntities(ListTag entities) { + checkRaw(); this.entities = entities; } @@ -370,6 +495,7 @@ public ListTag getTileEntities() { * @param tileEntities The tile entities of this chunk. */ public void setTileEntities(ListTag tileEntities) { + checkRaw(); this.tileEntities = tileEntities; } @@ -385,6 +511,7 @@ public ListTag getTileTicks() { * @param tileTicks Thee tile ticks. */ public void setTileTicks(ListTag tileTicks) { + checkRaw(); this.tileTicks = tileTicks; } @@ -400,6 +527,7 @@ public ListTag getLiquidTicks() { * @param liquidTicks The liquid ticks. */ public void setLiquidTicks(ListTag liquidTicks) { + checkRaw(); this.liquidTicks = liquidTicks; } @@ -415,11 +543,12 @@ public ListTag> getLights() { * @param lights The light sources. */ public void setLights(ListTag> lights) { + checkRaw(); this.lights = lights; } /** - * @return THe liquids to be ticked in this chunk. + * @return The liquids to be ticked in this chunk. */ public ListTag> getLiquidsToBeTicked() { return liquidsToBeTicked; @@ -430,6 +559,7 @@ public ListTag> getLiquidsToBeTicked() { * @param liquidsToBeTicked The liquids to be ticked. */ public void setLiquidsToBeTicked(ListTag> liquidsToBeTicked) { + checkRaw(); this.liquidsToBeTicked = liquidsToBeTicked; } @@ -445,6 +575,7 @@ public ListTag> getToBeTicked() { * @param toBeTicked The stuff to be ticked. */ public void setToBeTicked(ListTag> toBeTicked) { + checkRaw(); this.toBeTicked = toBeTicked; } @@ -460,6 +591,7 @@ public ListTag> getPostProcessing() { * @param postProcessing The things to be post processed. */ public void setPostProcessing(ListTag> postProcessing) { + checkRaw(); this.postProcessing = postProcessing; } @@ -475,6 +607,7 @@ public CompoundTag getStructures() { * @param structures The data about structures. */ public void setStructures(CompoundTag structures) { + checkRaw(); this.structures = structures; } @@ -483,49 +616,107 @@ int getBlockIndex(int blockX, int blockZ) { } public void cleanupPalettesAndBlockStates() { - for (Section section : sections) { + checkRaw(); + for (Section section : sections.values()) { if (section != null) { section.cleanupPaletteAndBlockStates(); } } } + private void checkRaw() { + if (raw) { + throw new UnsupportedOperationException("cannot update field when working with raw data"); + } + } + public static Chunk newChunk() { + return newChunk(DEFAULT_DATA_VERSION); + } + + public static Chunk newChunk(int dataVersion) { Chunk c = new Chunk(0); - c.dataVersion = DEFAULT_DATA_VERSION; + c.dataVersion = dataVersion; c.data = new CompoundTag(); c.data.put("Level", new CompoundTag()); c.status = "mobs_spawned"; return c; } + /** + * Provides a reference to the full chunk data. + * @return The full chunk data or null if there is none, e.g. when this chunk has only been loaded partially. + */ + public CompoundTag getHandle() { + return data; + } + public CompoundTag updateHandle(int xPos, int zPos) { + if (raw) { + return data; + } + data.putInt("DataVersion", dataVersion); CompoundTag level = data.getCompoundTag("Level"); level.putInt("xPos", xPos); level.putInt("zPos", zPos); level.putLong("LastUpdate", lastUpdate); level.putLong("InhabitedTime", inhabitedTime); - if (biomes != null && biomes.length == 256) level.putIntArray("Biomes", biomes); - if (heightMaps != null) level.put("HeightMaps", heightMaps); - if (carvingMasks != null) level.put("CarvingMasks", carvingMasks); - if (entities != null) level.put("Entities", entities); - if (tileEntities != null) level.put("TileEntities", tileEntities); - if (tileTicks != null) level.put("TileTicks", tileTicks); - if (liquidTicks != null) level.put("LiquidTicks", liquidTicks); - if (lights != null) level.put("Lights", lights); - if (liquidsToBeTicked != null) level.put("LiquidsToBeTicked", liquidsToBeTicked); - if (toBeTicked != null) level.put("ToBeTicked", toBeTicked); - if (postProcessing != null) level.put("PostProcessing", postProcessing); + if (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); + 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)); + for (Section section : this.sections.values()) { + if (section != null) { + sections.add(section.updateHandle()); } } level.put("Sections", sections); return data; } + + @Override + public Iterator
iterator() { + return sections.values().iterator(); + } } diff --git a/src/main/java/net/querz/nbt/mca/CompressionType.java b/src/main/java/net/querz/mca/CompressionType.java similarity index 98% rename from src/main/java/net/querz/nbt/mca/CompressionType.java rename to src/main/java/net/querz/mca/CompressionType.java index c93c1ba3..9ae41d4c 100644 --- a/src/main/java/net/querz/nbt/mca/CompressionType.java +++ b/src/main/java/net/querz/mca/CompressionType.java @@ -1,4 +1,4 @@ -package net.querz.nbt.mca; +package net.querz.mca; import java.io.IOException; import java.io.InputStream; diff --git a/src/main/java/net/querz/nbt/mca/ExceptionFunction.java b/src/main/java/net/querz/mca/ExceptionFunction.java similarity index 80% rename from src/main/java/net/querz/nbt/mca/ExceptionFunction.java rename to src/main/java/net/querz/mca/ExceptionFunction.java index 679eac99..40fe8195 100644 --- a/src/main/java/net/querz/nbt/mca/ExceptionFunction.java +++ b/src/main/java/net/querz/mca/ExceptionFunction.java @@ -1,4 +1,4 @@ -package net.querz.nbt.mca; +package net.querz.mca; @FunctionalInterface public interface ExceptionFunction { 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 00000000..da37a597 --- /dev/null +++ b/src/main/java/net/querz/mca/LoadFlags.java @@ -0,0 +1,23 @@ +package net.querz.mca; + +public final class LoadFlags { + + public static final long BIOMES = 0x00001; + public static final long HEIGHTMAPS = 0x00002; + public static final long CARVING_MASKS = 0x00004; + public static final long ENTITIES = 0x00008; + public static final long TILE_ENTITIES = 0x00010; + public static final long TILE_TICKS = 0x00040; + public static final long LIQUID_TICKS = 0x00080; + public static final long TO_BE_TICKED = 0x00100; + public static final long POST_PROCESSING = 0x00200; + public static final long STRUCTURES = 0x00400; + public static final long BLOCK_LIGHTS = 0x00800; + public static final long BLOCK_STATES = 0x01000; + public static final long SKY_LIGHT = 0x02000; + public static final long LIGHTS = 0x04000; + public static final long LIQUIDS_TO_BE_TICKED = 0x08000; + public static final long RAW = 0x10000; + + public static final long ALL_DATA = 0xffffffffffffffffL; +} diff --git a/src/main/java/net/querz/nbt/mca/MCAFile.java b/src/main/java/net/querz/mca/MCAFile.java similarity index 82% rename from src/main/java/net/querz/nbt/mca/MCAFile.java rename to src/main/java/net/querz/mca/MCAFile.java index 2ed23de4..0a6e7122 100644 --- a/src/main/java/net/querz/nbt/mca/MCAFile.java +++ b/src/main/java/net/querz/mca/MCAFile.java @@ -1,10 +1,15 @@ -package net.querz.nbt.mca; +package net.querz.mca; + +import net.querz.nbt.tag.CompoundTag; +import net.querz.nbt.tag.Tag; -import net.querz.nbt.CompoundTag; import java.io.IOException; import java.io.RandomAccessFile; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Map; -public class MCAFile { +public class MCAFile implements Iterable { /** * The default chunk data version used when no custom version is supplied. @@ -34,6 +39,17 @@ public MCAFile(int regionX, int regionZ) { * @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); @@ -47,7 +63,7 @@ public void deserialize(RandomAccessFile raf) throws IOException { int timestamp = raf.readInt(); Chunk chunk = new Chunk(timestamp); raf.seek(4096 * offset + 4); //+4: skip data size - chunk.deserialize(raf); + chunk.deserialize(raf, loadFlags); chunks[i] = chunk; } } @@ -100,7 +116,7 @@ public int serialize(RandomAccessFile raf, boolean changeLastUpdate) throws IOEx chunksWritten++; - int sectors = (lastWritten >> 12) + 1; + int sectors = (lastWritten >> 12) + (lastWritten % 4096 == 0 ? 0 : 1); raf.seek(index * 4); raf.writeByte(globalOffset >>> 16); @@ -201,22 +217,21 @@ private Chunk createChunkIfMissing(int blockX, int blockZ) { } /** - * Sets the biome at a specific block column. - * A negative number will be replaced by the columns default biome when loaded by Minecraft. - * @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. + * @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); + } + /** - * Fetches the biome id at a specific block column. - * @param blockX The x-coordinate of the block column. - * @param blockZ The z-coordinate of the block column. - * @return The biome id if the chunk exists and the chunk has biomes, otherwise -1. + * @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)); @@ -226,6 +241,22 @@ public int getBiomeAt(int blockX, int blockZ) { 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. @@ -266,4 +297,9 @@ public void cleanupPalettesAndBlockStates() { } } } + + @Override + public Iterator iterator() { + return Arrays.stream(chunks).iterator(); + } } diff --git a/src/main/java/net/querz/nbt/mca/MCAUtil.java b/src/main/java/net/querz/mca/MCAUtil.java similarity index 74% rename from src/main/java/net/querz/nbt/mca/MCAUtil.java rename to src/main/java/net/querz/mca/MCAUtil.java index fdd7142a..f5ddecc5 100644 --- a/src/main/java/net/querz/nbt/mca/MCAUtil.java +++ b/src/main/java/net/querz/mca/MCAUtil.java @@ -1,4 +1,4 @@ -package net.querz.nbt.mca; +package net.querz.mca; import java.io.File; import java.io.IOException; @@ -17,13 +17,13 @@ public final class MCAUtil { private MCAUtil() {} /** - * @see MCAUtil#readMCAFile(File) + * @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 readMCAFile(String file) throws IOException { - return readMCAFile(new File(file)); + public static MCAFile read(String file) throws IOException { + return read(new File(file), LoadFlags.ALL_DATA); } /** @@ -32,48 +32,70 @@ public static MCAFile readMCAFile(String file) throws IOException { * @return An in-memory representation of the MCA file with decompressed chunk data. * @throws IOException if something during deserialization goes wrong. * */ - public static MCAFile readMCAFile(File file) throws IOException { + 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); + mcaFile.deserialize(raf, loadFlags); return mcaFile; } } /** - * Calls {@link MCAUtil#writeMCAFile(MCAFile, File, boolean)} without changing the timestamps. - * @see MCAUtil#writeMCAFile(MCAFile, File, boolean) + * 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 writeMCAFile(MCAFile mcaFile, String file) throws IOException { - return writeMCAFile(mcaFile, new File(file), false); + public static int write(MCAFile mcaFile, String file) throws IOException { + return write(mcaFile, new File(file), false); } /** - * Calls {@link MCAUtil#writeMCAFile(MCAFile, File, boolean)} without changing the timestamps. - * @see MCAUtil#writeMCAFile(MCAFile, File, boolean) + * 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 writeMCAFile(MCAFile mcaFile, File file) throws IOException { - return writeMCAFile(mcaFile, file, false); + public static int write(MCAFile mcaFile, File file) throws IOException { + return write(mcaFile, file, false); } /** - * @see MCAUtil#writeMCAFile(MCAFile, File, boolean) + * @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 writeMCAFile(MCAFile mcaFile, String file, boolean changeLastUpdate) throws IOException { - return writeMCAFile(mcaFile, new File(file), changeLastUpdate); + public static int write(MCAFile mcaFile, String file, boolean changeLastUpdate) throws IOException { + return write(mcaFile, new File(file), changeLastUpdate); } /** @@ -87,7 +109,7 @@ public static int writeMCAFile(MCAFile mcaFile, String file, boolean changeLastU * @return The amount of chunks written to the file. * @throws IOException If something goes wrong during serialization. * */ - public static int writeMCAFile(MCAFile mcaFile, File file, boolean changeLastUpdate) throws IOException { + public static int write(MCAFile mcaFile, File file, boolean changeLastUpdate) throws IOException { File to = file; if (file.exists()) { to = File.createTempFile(to.getName(), null); @@ -191,7 +213,7 @@ public static int chunkToBlock(int chunk) { private static final Pattern mcaFilePattern = Pattern.compile("^.*r\\.(?-?\\d+)\\.(?-?\\d+)\\.mca$"); - private static MCAFile newMCAFile(File file) { + 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"))); diff --git a/src/main/java/net/querz/nbt/mca/Section.java b/src/main/java/net/querz/mca/Section.java similarity index 62% rename from src/main/java/net/querz/nbt/mca/Section.java rename to src/main/java/net/querz/mca/Section.java index 649a40d3..3b72b969 100644 --- a/src/main/java/net/querz/nbt/mca/Section.java +++ b/src/main/java/net/querz/mca/Section.java @@ -1,13 +1,17 @@ -package net.querz.nbt.mca; +package net.querz.mca; -import net.querz.nbt.CompoundTag; -import net.querz.nbt.ListTag; +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.Iterator; import java.util.List; import java.util.Map; -public class Section { +public class Section implements Comparable
{ private CompoundTag data; private Map> valueIndexedPalette = new HashMap<>(); @@ -15,12 +19,18 @@ public class Section { private byte[] blockLight; private long[] blockStates; private byte[] skyLight; + private int height; + 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; + height = sectionRoot.getNumber("Y").byteValue(); - /** - * Creates a new Section based on raw section data. - * @param sectionRoot The raw section data - */ - public Section(CompoundTag sectionRoot) { ListTag rawPalette = sectionRoot.getListTag("Palette"); if (rawPalette == null) { return; @@ -30,15 +40,25 @@ public Section(CompoundTag sectionRoot) { CompoundTag data = palette.get(i); putValueIndexedPalette(data, i); } - blockLight = sectionRoot.getByteArray("BlockLight"); - blockStates = sectionRoot.getLongArray("BlockStates"); - skyLight = sectionRoot.getByteArray("SkyLight"); - data = sectionRoot; + + 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() {} - private void putValueIndexedPalette(CompoundTag data, int index) { + void putValueIndexedPalette(CompoundTag data, int index) { PaletteIndex leaf = new PaletteIndex(data, index); String name = data.getString("Name"); List leaves = valueIndexedPalette.get(name); @@ -56,7 +76,7 @@ private void putValueIndexedPalette(CompoundTag data, int index) { } } - private PaletteIndex getValueIndexedPalette(CompoundTag data) { + PaletteIndex getValueIndexedPalette(CompoundTag data) { List leaves = valueIndexedPalette.get(data.getString("Name")); if (leaves == null) { return null; @@ -69,7 +89,15 @@ private PaletteIndex getValueIndexedPalette(CompoundTag data) { return null; } - private class PaletteIndex { + @Override + public int compareTo(Section o) { + if (o == null) { + return -1; + } + return Integer.compare(height, o.height); + } + + private static class PaletteIndex { CompoundTag data; int index; @@ -88,6 +116,17 @@ public boolean isEmpty() { return data == null; } + /** + * @return the Y value of this section. + * */ + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = height; + } + /** * Fetches a block state based on a block location from this section. * The coordinates represent the location of the block inside of this Section. @@ -97,7 +136,10 @@ public boolean isEmpty() { * @return The block state data of this block. */ public CompoundTag getBlockStateAt(int blockX, int blockY, int blockZ) { - int index = getBlockIndex(blockX, blockY, blockZ); + return getBlockStateAt(getBlockIndex(blockX, blockY, blockZ)); + } + + private CompoundTag getBlockStateAt(int index) { int paletteIndex = getPaletteIndex(index); return palette.get(paletteIndex); } @@ -137,15 +179,23 @@ public void setBlockStateAt(int blockX, int blockY, int blockZ, CompoundTag stat * */ public int getPaletteIndex(int blockStateIndex) { int bits = blockStates.length >> 6; - 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); + + 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 { - return (int) bitRange(blockStates[longIndex], startBit, startBit + bits); + int indicesPerLong = (int) (64D / bits); + int blockStatesIndex = blockStateIndex / indicesPerLong; + int startBit = (blockStateIndex % indicesPerLong) * bits; + return (int) bitRange(blockStates[blockStatesIndex], startBit, startBit + bits); } } @@ -156,15 +206,23 @@ public int getPaletteIndex(int blockStateIndex) { * @param blockStates The block states to be updated. * */ public void setPaletteIndex(int blockIndex, int paletteIndex, long[] blockStates) { - int bits = blockStates.length / 64; - 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); + 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 { - blockStates[longIndex] = updateBits(blockStates[longIndex], paletteIndex, startBit, startBit + bits); + int indicesPerLong = (int) (64D / bits); + int blockStatesIndex = blockIndex / indicesPerLong; + int startBit = (blockIndex % indicesPerLong) * bits; + blockStates[blockStatesIndex] = updateBits(blockStates[blockStatesIndex], paletteIndex, startBit, startBit + bits); } } @@ -186,17 +244,17 @@ int addToPalette(CompoundTag data) { return palette.size() - 1; } - private int getBlockIndex(int blockX, int blockY, int blockZ) { + int getBlockIndex(int blockX, int blockY, int blockZ) { return (blockY & 0xF) * 256 + (blockZ & 0xF) * 16 + (blockX & 0xF); } - private static long updateBits(long n, long m, int i, int j) { + 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); } - private static long bitRange(long value, int from, int to) { + static long bitRange(long value, int from, int to) { int waste = 64 - to; return (value << waste) >>> (waste + from); } @@ -207,8 +265,10 @@ private static long bitRange(long value, int from, int to) { * Recalculating the Palette should only be executed once right before saving the Section to file. */ public void cleanupPaletteAndBlockStates() { - Map oldToNewMapping = cleanupPalette(); - adjustBlockStateBits(oldToNewMapping, blockStates); + if (blockStates != null) { + Map oldToNewMapping = cleanupPalette(); + adjustBlockStateBits(oldToNewMapping, blockStates); + } } private Map cleanupPalette() { @@ -237,15 +297,22 @@ private Map cleanupPalette() { return allIndices; } - private void adjustBlockStateBits(Map oldToNewMapping, long[] blockStates) { + 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 = newBits < 4 ? 4 : newBits; + newBits = Math.max(newBits, 4); + + long[] newBlockStates; - long[] newBlockStates = newBits == blockStates.length / 64 ? blockStates : new long[newBits * 64]; + if (dataVersion < 2527) { + newBlockStates = newBits == blockStates.length / 64 ? blockStates : new long[newBits * 64]; + } else { + int newLength = (int) Math.ceil(4096D / (Math.floor(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); @@ -342,10 +409,65 @@ public static Section newSection() { */ public CompoundTag updateHandle(int y) { data.putByte("Y", (byte) y); - data.put("Palette", palette); - if (blockLight != null) data.putByteArray("BlockLight", blockLight); - data.putLongArray("BlockStates", blockStates); - if (skyLight != null) data.putByteArray("SkyLight", skyLight); + 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; } + + public CompoundTag updateHandle() { + return updateHandle(height); + } + + /** + * Creates an iterable that iterates over all blocks in this section, in order of their indices. + * An index can be calculated using the following formula: + *
+	 * {@code
+	 * index = (blockY & 0xF) * 256 + (blockZ & 0xF) * 16 + (blockX & 0xF);
+	 * }
+	 * 
+ * The CompoundTags are references to this Section's Palette and should only be modified if the intention is to + * modify ALL blocks of the same type in this Section at the same time. + * */ + public Iterable blocksStates() { + return new BlockIterator(this); + } + + private static class BlockIterator implements Iterable, Iterator { + + private Section section; + private int currentIndex; + + public BlockIterator(Section section) { + this.section = section; + currentIndex = 0; + } + + @Override + public boolean hasNext() { + return currentIndex < 4096; + } + + @Override + public CompoundTag next() { + CompoundTag blockState = section.getBlockStateAt(currentIndex); + currentIndex++; + return blockState; + } + + @Override + public Iterator iterator() { + return this; + } + } } diff --git a/src/main/java/net/querz/nbt/EndTag.java b/src/main/java/net/querz/nbt/EndTag.java deleted file mode 100644 index b07a8af4..00000000 --- a/src/main/java/net/querz/nbt/EndTag.java +++ /dev/null @@ -1,43 +0,0 @@ -package net.querz.nbt; - -import java.io.DataInputStream; -import java.io.DataOutputStream; - -public final class EndTag extends Tag { - - static final EndTag INSTANCE = new EndTag(); - - private EndTag() { - super(null); - } - - @Override - protected Void checkValue(Void value) { - return value; - } - - @Override - public void serializeValue(DataOutputStream dos, int maxDepth) { - //nothing to do - } - - @Override - public void deserializeValue(DataInputStream dis, int maxDepth) { - //nothing to do - } - - @Override - public String valueToString(int maxDepth) { - return "\"end\""; - } - - @Override - public String valueToTagString(int maxDepth) { - throw new UnsupportedOperationException("EndTag cannot be turned into a String"); - } - - @Override - public EndTag clone() { - return INSTANCE; - } -} diff --git a/src/main/java/net/querz/nbt/NBTUtil.java b/src/main/java/net/querz/nbt/NBTUtil.java deleted file mode 100644 index 3779e795..00000000 --- a/src/main/java/net/querz/nbt/NBTUtil.java +++ /dev/null @@ -1,172 +0,0 @@ -package net.querz.nbt; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -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; -import java.util.zip.GZIPOutputStream; - -/** - * Provides main functions to read Tags from and write Tags to nbt files. - * This does not support reading or writing .mca files, use {@link net.querz.nbt.mca.MCAUtil} instead. - * */ -public final class NBTUtil { - - private NBTUtil() {} - - /** - * Calls {@link NBTUtil#writeTag(Tag, String, File, boolean)} with an empty name and GZIP compression. - * @see NBTUtil#writeTag(Tag, String, File, boolean) - * @param tag The tag to be written to the file. - * @param file The file to write {@code tag} into. - * @throws IOException If something during the serialization goes wrong. - * @throws NullPointerException If {@code tag}, {@code name} or {@code file} is {@code null}. - * @throws MaxDepthReachedException If the NBT structure exceeds {@link Tag#DEFAULT_MAX_DEPTH}. - */ - public static void writeTag(Tag tag, String file) throws IOException { - writeTag(tag, "", new File(file), true); - } - - /** - * Calls {@link NBTUtil#writeTag(Tag, String, File, boolean)} with an empty name and GZIP compression. - * @see NBTUtil#writeTag(Tag, String, File, boolean) - * @param tag The tag to be written to the file. - * @param file The file to write {@code tag} into. - * @throws IOException If something during the serialization goes wrong. - * @throws NullPointerException If {@code tag}, {@code name} or {@code file} is {@code null}. - * @throws MaxDepthReachedException If the NBT structure exceeds {@link Tag#DEFAULT_MAX_DEPTH}. - */ - public static void writeTag(Tag tag, File file) throws IOException { - writeTag(tag, "", file, true); - } - - /** - * Calls {@link NBTUtil#writeTag(Tag, String, File, boolean)} with an empty name. - * @see NBTUtil#writeTag(Tag, String, File, boolean) - * @param tag The tag to be written to the file. - * @param file The file to write {@code tag} into. - * @param compressed {@code true} if the file should be GZIP compressed, {@code false} if not. - * @throws IOException If something during the serialization goes wrong. - * @throws NullPointerException If {@code tag}, {@code name} or {@code file} is {@code null}. - * @throws MaxDepthReachedException If the NBT structure exceeds {@link Tag#DEFAULT_MAX_DEPTH}. - */ - public static void writeTag(Tag tag, String file, boolean compressed) throws IOException { - writeTag(tag, "", new File(file), compressed); - } - - /** - * Calls {@link NBTUtil#writeTag(Tag, String, File, boolean)} with an empty name. - * @see NBTUtil#writeTag(Tag, String, File, boolean) - * @param tag The tag to be written to the file. - * @param file The file to write {@code tag} into. - * @param compressed {@code true} if the file should be GZIP compressed, {@code false} if not. - * @throws IOException If something during the serialization goes wrong. - * @throws NullPointerException If {@code tag}, {@code name} or {@code file} is {@code null}. - * @throws MaxDepthReachedException If the NBT structure exceeds {@link Tag#DEFAULT_MAX_DEPTH}. - * */ - public static void writeTag(Tag tag, File file, boolean compressed) throws IOException { - writeTag(tag, "", file, compressed); - } - - /** - * @see NBTUtil#writeTag(Tag, String, File) - * @param tag The tag to be written to the file. - * @param name The name of the root tag. - * @param file The file to write {@code tag} into. - * @throws IOException If something during the serialization goes wrong. - * @throws NullPointerException If {@code tag}, {@code name} or {@code file} is {@code null}. - * @throws MaxDepthReachedException If the NBT structure exceeds {@link Tag#DEFAULT_MAX_DEPTH}. - */ - public static void writeTag(Tag tag, String name, String file) throws IOException { - writeTag(tag, name, new File(file), true); - } - - /** - * Calls {@link NBTUtil#writeTag(Tag, String, String, boolean)} with GZIP compression. - * @see NBTUtil#writeTag(Tag, String, File, boolean) - * @param tag The tag to be written to the file. - * @param name The name of the root tag. - * @param file The file to write {@code tag} into. - * @throws IOException If something during the serialization goes wrong. - * @throws NullPointerException If {@code tag}, {@code name} or {@code file} is {@code null}. - * @throws MaxDepthReachedException If the NBT structure exceeds {@link Tag#DEFAULT_MAX_DEPTH}. - */ - public static void writeTag(Tag tag, String name, File file) throws IOException { - writeTag(tag, name, file, true); - } - - /** - * @see NBTUtil#writeTag(Tag, String, String, boolean) - * @param tag The tag to be written to the file. - * @param name The name of the root tag. - * @param file The file to write {@code tag} into. - * @param compressed {@code true} if the file should be GZIP compressed, {@code false} if not. - * @throws IOException If something during the serialization goes wrong. - * @throws NullPointerException If {@code tag}, {@code name} or {@code file} is {@code null}. - * @throws MaxDepthReachedException If the NBT structure exceeds {@link Tag#DEFAULT_MAX_DEPTH}. - */ - public static void writeTag(Tag tag, String name, String file, boolean compressed) throws IOException { - writeTag(tag, name, new File(file), compressed); - } - - /** - * Writes the Tag {@code tag} to File {@code file}. A {@code name} for the root tag can be specified, - * but will be ignored during deserialization. Also allows for optional GZIP compression. - * @param tag The tag to be written to the file. - * @param name The name of the root tag. - * @param file The file to write {@code tag} into. - * @param compressed {@code true} if the file should be GZIP compressed, {@code false} if not. - * @throws IOException If something during the serialization goes wrong. - * @throws NullPointerException If {@code tag}, {@code name} or {@code file} is {@code null}. - * @throws MaxDepthReachedException If the NBT structure exceeds {@link Tag#DEFAULT_MAX_DEPTH}. - */ - public static void writeTag(Tag tag, String name, File file, boolean compressed) throws IOException { - try ( - DataOutputStream dos = new DataOutputStream( - compressed ? new GZIPOutputStream(new FileOutputStream(file)) : new FileOutputStream(file)) - ) { - tag.serialize(dos, name, Tag.DEFAULT_MAX_DEPTH); - } - } - - /** - * @see NBTUtil#readTag(File) - * @param file The file to read a Tag from. - * @return The tag read from the file. - * @throws IOException If something during deserialization goes wrong. - * */ - public static Tag readTag(String file) throws IOException { - return readTag(new File(file)); - } - - /** - * Reads a Tag from the specified file. Automatically detects GZIP detection - * and decompresses the stream if necessary. - * @param file The file to read a Tag from. - * @return The tag read from the file. - * @throws IOException If something during deserialization goes wrong. - * @throws NullPointerException If {@code file} is {@code null}. - * @throws MaxDepthReachedException If the NBT structure exceeds {@link Tag#DEFAULT_MAX_DEPTH}. - * */ - public static Tag readTag(File file) throws IOException { - try (DataInputStream dis = new DataInputStream(applyDecompression(new FileInputStream(file)))) { - return Tag.deserialize(dis, Tag.DEFAULT_MAX_DEPTH); - } - } - - static InputStream applyDecompression(InputStream is) throws IOException { - PushbackInputStream pbis = new PushbackInputStream(is, 2); - int sig = (pbis.read() & 0xFF) + (pbis.read() << 8); - pbis.unread(sig >> 8); - pbis.unread(sig & 0xFF); - if (sig == GZIPInputStream.GZIP_MAGIC) { - return new GZIPInputStream(pbis); - } - return pbis; - } -} diff --git a/src/main/java/net/querz/nbt/TagFactory.java b/src/main/java/net/querz/nbt/TagFactory.java deleted file mode 100644 index cfd5a65e..00000000 --- a/src/main/java/net/querz/nbt/TagFactory.java +++ /dev/null @@ -1,101 +0,0 @@ -package net.querz.nbt; - -import java.util.HashMap; -import java.util.Map; -import java.util.function.Supplier; - -public final class TagFactory { - - private static class TagMapping> { - - private int id; - private Supplier factory; - private Class clazz; - - TagMapping(int id, Supplier factory, Class clazz) { - this.id = id; - this.factory = factory; - this.clazz = clazz; - } - } - - private static Map> idMapping = new HashMap<>(); - private static Map, TagMapping> classMapping = new HashMap<>(); - static { - put(0, () -> EndTag.INSTANCE, EndTag.class); - put(1, ByteTag::new, ByteTag.class); - put(2, ShortTag::new, ShortTag.class); - put(3, IntTag::new, IntTag.class); - put(4, LongTag::new, LongTag.class); - put(5, FloatTag::new, FloatTag.class); - put(6, DoubleTag::new, DoubleTag.class); - put(7, ByteArrayTag::new, ByteArrayTag.class); - put(8, StringTag::new, StringTag.class); - put(9, ListTag::createUnchecked, ListTag.class); - put(10, CompoundTag::new, CompoundTag.class); - put(11, IntArrayTag::new, IntArrayTag.class); - put(12, LongArrayTag::new, LongArrayTag.class); - } - - private static > void put(int id, Supplier factory, Class clazz) { - TagMapping mapping = new TagMapping<>(id, factory, clazz); - idMapping.put(id, mapping); - classMapping.put(clazz, mapping); - } - - private TagFactory() {} - - public static Tag fromID(int id) { - TagMapping mapping = idMapping.get(id); - if (mapping == null) { - throw new IllegalArgumentException("unknown Tag id " + id); - } - return mapping.factory.get(); - } - - public static Class classFromID(int id) { - TagMapping mapping = idMapping.get(id); - if (mapping == null) { - throw new IllegalArgumentException("unknown Tag id " + id); - } - return mapping.clazz; - } - - public static byte idFromClass(Class clazz) { - TagMapping mapping = classMapping.get(clazz); - if (mapping == null) { - throw new IllegalArgumentException("unknown Tag class " + clazz.getName()); - } - return (byte) mapping.id; - } - - public static > void registerCustomTag(int id, Supplier factory, Class clazz) { - checkID(id); - if (idMapping.containsKey(id)) { - throw new IllegalArgumentException("custom tag already registered"); - } - put(id, factory, clazz); - } - - public static void unregisterCustomTag(int id) { - idMapping.remove(id); - for (TagMapping mapping : classMapping.values()) { - if (mapping.id == id) { - classMapping.remove(mapping.clazz); - return; - } - } - } - - private static void checkID(int id) { - if (id < 0) { - throw new IllegalArgumentException("id cannot be negative"); - } - if (id <= 12) { - throw new IllegalArgumentException("cannot change default tags"); - } - if (id > Byte.MAX_VALUE) { - throw new IllegalArgumentException("id out of bounds: " + id); - } - } -} diff --git a/src/main/java/net/querz/nbt/custom/CharTag.java b/src/main/java/net/querz/nbt/custom/CharTag.java deleted file mode 100644 index cba9654c..00000000 --- a/src/main/java/net/querz/nbt/custom/CharTag.java +++ /dev/null @@ -1,68 +0,0 @@ -package net.querz.nbt.custom; - -import net.querz.nbt.Tag; -import net.querz.nbt.TagFactory; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -public class CharTag extends Tag implements Comparable { - - public static final char ZERO_VALUE = '\u0000'; - - public static void register() { - TagFactory.registerCustomTag(110, CharTag::new, CharTag.class); - } - - public CharTag() { - super(ZERO_VALUE); - } - - public CharTag(char value) { - super(value); - } - - @Override - public Character getValue() { - return super.getValue(); - } - - public void setValue(char value) { - super.setValue(value); - } - - @Override - public void serializeValue(DataOutputStream dos, int maxDepth) throws IOException { - dos.writeChar(getValue()); - } - - @Override - public void deserializeValue(DataInputStream dis, int maxDepth) throws IOException { - setValue(dis.readChar()); - } - - @Override - public String valueToString(int maxDepth) { - return escapeString(getValue() + "", false); - } - - @Override - public String valueToTagString(int maxDepth) { - return escapeString(getValue() + "", true); - } - - @Override - public boolean equals(Object other) { - return super.equals(other) && getValue() == ((CharTag) other).getValue(); - } - - @Override - public int compareTo(CharTag o) { - return Character.compare(getValue(), o.getValue()); - } - - @Override - public CharTag clone() { - return new CharTag(getValue()); - } -} diff --git a/src/main/java/net/querz/nbt/custom/ObjectTag.java b/src/main/java/net/querz/nbt/custom/ObjectTag.java deleted file mode 100644 index 92ea5624..00000000 --- a/src/main/java/net/querz/nbt/custom/ObjectTag.java +++ /dev/null @@ -1,124 +0,0 @@ -package net.querz.nbt.custom; - -import net.querz.nbt.Tag; -import net.querz.nbt.TagFactory; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.io.InvalidClassException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.Serializable; -import java.lang.reflect.InvocationTargetException; -import java.util.Objects; - -public class ObjectTag extends Tag implements Comparable> { - - public static void register() { - TagFactory.registerCustomTag(90, ObjectTag::new, ObjectTag.class); - } - - public ObjectTag() { - super(null); - } - - public ObjectTag(T value) { - super(value); - } - - @Override - protected T checkValue(T value) { - return value; - } - - @Override - public T getValue() { - return super.getValue(); - } - - @Override - public void setValue(T value) { - super.setValue(value); - } - - @SuppressWarnings("unchecked") - public ObjectTag asTypedObjectTag(Class type) { - checkTypeClass(type); - return (ObjectTag) this; - } - - @Override - public void serializeValue(DataOutputStream dos, int maxDepth) throws IOException { - new ObjectOutputStream(dos).writeObject(getValue()); - } - - @SuppressWarnings("unchecked") - @Override - public void deserializeValue(DataInputStream dis, int maxDepth) throws IOException { - try { - setValue((T) new ObjectInputStream(dis).readObject()); - } catch (InvalidClassException | ClassNotFoundException e) { - throw new IOException(e.getCause()); - } - } - - @Override - public String valueToString(int maxDepth) { - return getValue() == null ? "null" : escapeString(getValue().toString(), false); - } - - @Override - public String valueToTagString(int maxDepth) { - return getValue() == null ? "null" : escapeString(getValue().toString(), true); - } - - @Override - public boolean equals(Object other) { - return super.equals(other) && Objects.equals(getValue(), ((ObjectTag) other).getValue()); - } - - @Override - public int hashCode() { - if (getValue() == null) { - return 0; - } - return getValue().hashCode(); - } - - @SuppressWarnings("unchecked") - @Override - public int compareTo(ObjectTag o) { - if (o.getValue() instanceof Comparable && getValue() instanceof Comparable) { - return ((Comparable) getValue()).compareTo(o.getValue()); - } else if (o.getValue() == getValue()) { - return 0; - } else if (getValue() == null) { - // sort a null value to the end - return 1; - } else if (o.getValue() == null) { - return -1; - } - return 0; - } - - @SuppressWarnings("unchecked") - @Override - public ObjectTag clone() { - if (getValue() == null) { - return new ObjectTag<>(); - } - try { - return new ObjectTag<>((T) getValue().getClass().getMethod("clone").invoke(getValue())); - } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { - return new ObjectTag<>(getValue()); - } - } - - private void checkTypeClass(Class clazz) { - if (getValue() != null && (!clazz.isAssignableFrom(getValue().getClass()))) { - throw new ClassCastException(String.format( - "cannot cast ObjectTag<%s> to ObjectTag<%s>", - getValue().getClass().getSimpleName(), clazz.getSimpleName())); - } - } -} diff --git a/src/main/java/net/querz/nbt/custom/ShortArrayTag.java b/src/main/java/net/querz/nbt/custom/ShortArrayTag.java deleted file mode 100644 index ba46d33e..00000000 --- a/src/main/java/net/querz/nbt/custom/ShortArrayTag.java +++ /dev/null @@ -1,70 +0,0 @@ -package net.querz.nbt.custom; - -import net.querz.nbt.ArrayTag; -import net.querz.nbt.TagFactory; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.util.Arrays; - -public class ShortArrayTag extends ArrayTag implements Comparable { - - public static final short[] ZERO_VALUE = new short[0]; - - public static void register() { - TagFactory.registerCustomTag(100, ShortArrayTag::new, ShortArrayTag.class); - } - - public ShortArrayTag() { - super(ZERO_VALUE); - } - - public ShortArrayTag(short[] value) { - super(value); - } - - @Override - public void serializeValue(DataOutputStream dos, int maxDepth) throws IOException { - dos.writeInt(length()); - for (int i : getValue()) { - dos.writeShort(i); - } - } - - @Override - public void deserializeValue(DataInputStream dis, int maxDepth) throws IOException { - int length = dis.readInt(); - setValue(new short[length]); - for (int i = 0; i < length; i++) { - getValue()[i] = dis.readShort(); - } - } - - @Override - public String valueToTagString(int maxDepth) { - return arrayToString("S", "s"); - } - - @Override - public boolean equals(Object other) { - return super.equals(other) - && (getValue() == ((ShortArrayTag) other).getValue() - || getValue().length == (((ShortArrayTag) other).length()) - && Arrays.equals(getValue(), ((ShortArrayTag) other).getValue())); - } - - @Override - public int hashCode() { - return Arrays.hashCode(getValue()); - } - - @Override - public int compareTo(ShortArrayTag other) { - return Integer.compare(length(), other.length()); - } - - @Override - public ShortArrayTag clone() { - return new ShortArrayTag(Arrays.copyOf(getValue(), length())); - } -} diff --git a/src/main/java/net/querz/nbt/custom/StructTag.java b/src/main/java/net/querz/nbt/custom/StructTag.java deleted file mode 100644 index 47c7215c..00000000 --- a/src/main/java/net/querz/nbt/custom/StructTag.java +++ /dev/null @@ -1,305 +0,0 @@ -package net.querz.nbt.custom; - -import net.querz.nbt.ByteArrayTag; -import net.querz.nbt.ByteTag; -import net.querz.nbt.CompoundTag; -import net.querz.nbt.DoubleTag; -import net.querz.nbt.FloatTag; -import net.querz.nbt.IntArrayTag; -import net.querz.nbt.IntTag; -import net.querz.nbt.ListTag; -import net.querz.nbt.LongArrayTag; -import net.querz.nbt.LongTag; -import net.querz.nbt.ShortTag; -import net.querz.nbt.StringTag; -import net.querz.nbt.Tag; -import net.querz.nbt.TagFactory; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.Objects; -import java.util.function.Consumer; - -public class StructTag extends Tag>> implements Iterable>, Comparable { - - public static void register() { - TagFactory.registerCustomTag(120, StructTag::new, StructTag.class); - } - - public StructTag() { - super(createEmptyValue()); - } - - private static List> createEmptyValue() { - return new ArrayList<>(3); - } - - public int size() { - return getValue().size(); - } - - public Tag remove(int index) { - return getValue().remove(index); - } - - public boolean remove(Tag tag) { - return getValue().remove(tag); - } - - public void clear() { - getValue().clear(); - } - - public boolean contains(Tag tag) { - return getValue().contains(tag); - } - - public boolean containsAll(Collection> tags) { - return getValue().containsAll(tags); - } - - @Override - public Iterator> iterator() { - return getValue().iterator(); - } - - @Override - public void forEach(Consumer> action) { - getValue().forEach(action); - } - - public > S get(int index, Class type) { - Tag t = getValue().get(index); - return type.cast(t); - } - - public Tag get(int index) { - return getValue().get(index); - } - - public ByteTag getByteTag(int index) { - return get(index, ByteTag.class); - } - - public ShortTag getShortTag(int index) { - return get(index, ShortTag.class); - } - - public IntTag getIntTag(int index) { - return get(index, IntTag.class); - } - - public LongTag getLongTag(int index) { - return get(index, LongTag.class); - } - - public FloatTag getFloatTag(int index) { - return get(index, FloatTag.class); - } - - public DoubleTag getDoubleTag(int index) { - return get(index, DoubleTag.class); - } - - public StringTag getStringTag(int index) { - return get(index, StringTag.class); - } - - public ByteArrayTag getByteArrayTag(int index) { - return get(index, ByteArrayTag.class); - } - - public IntArrayTag getIntArrayTag(int index) { - return get(index, IntArrayTag.class); - } - - public LongArrayTag getLongArrayTag(int index) { - return get(index, LongArrayTag.class); - } - - public ListTag getListTag(int index) { - return get(index, ListTag.class); - } - - public CompoundTag getCompoundTag(int index) { - return get(index, CompoundTag.class); - } - - public boolean getBoolean(int index) { - Tag t = get(index); - return t instanceof ByteTag && ((ByteTag) t).asByte() > 0; - } - - public byte getByte(int index) { - return getByteTag(index).asByte(); - } - - public short getShort(int index) { - return getShortTag(index).asShort(); - } - - public int getInt(int index) { - return getIntTag(index).asInt(); - } - - public long getLong(int index) { - return getLongTag(index).asLong(); - } - - public float getFloat(int index) { - return getFloatTag(index).asFloat(); - } - - public double getDouble(int index) { - return getDoubleTag(index).asDouble(); - } - - public String getString(int index) { - return getStringTag(index).getValue(); - } - - public byte[] getByteArray(int index) { - return getByteArrayTag(index).getValue(); - } - - public int[] getIntArray(int index) { - return getIntArrayTag(index).getValue(); - } - - public long[] getLongArray(int index) { - return getLongArrayTag(index).getValue(); - } - - public Tag set(int index, Tag tag) { - return getValue().set(index, Objects.requireNonNull(tag)); - } - - public void add(Tag tag) { - getValue().add(Objects.requireNonNull(tag)); - } - - public void add(int index, Tag tag) { - getValue().add(index, Objects.requireNonNull(tag)); - } - - public void addBoolean(boolean value) { - add(new ByteTag(value)); - } - - public void addByte(byte value) { - add(new ByteTag(value)); - } - - public void addShort(short value) { - add(new ShortTag(value)); - } - - public void addInt(int value) { - add(new IntTag(value)); - } - - public void addLong(long value) { - add(new LongTag(value)); - } - - public void addFloat(float value) { - add(new FloatTag(value)); - } - - public void addDouble(double value) { - add(new DoubleTag(value)); - } - - public void addString(String value) { - add(new StringTag(value)); - } - - public void addByteArray(byte[] value) { - add(new ByteArrayTag(value)); - } - - public void addIntArray(int[] value) { - add(new IntArrayTag(value)); - } - - public void addLongArray(long[] value) { - add(new LongArrayTag(value)); - } - - @Override - public void serializeValue(DataOutputStream dos, int maxDepth) throws IOException { - dos.writeInt(size()); - for (Tag tag : getValue()) { - dos.writeByte(tag.getID()); - tag.serializeValue(dos, decrementMaxDepth(maxDepth)); - } - } - - @Override - public void deserializeValue(DataInputStream dis, int maxDepth) throws IOException { - int size = dis.readInt(); - size = size < 0 ? 0 : size; - setValue(new ArrayList<>(size)); - for (int i = 0; i < size; i++) { - Tag tag = TagFactory.fromID(dis.readByte()); - tag.deserializeValue(dis, decrementMaxDepth(maxDepth)); - add(tag); - } - } - - @Override - public String valueToString(int maxDepth) { - StringBuilder sb = new StringBuilder("["); - for (int i = 0; i < size(); i++) { - sb.append(i > 0 ? "," : "").append(get(i).toString(decrementMaxDepth(maxDepth))); - } - sb.append("]"); - return sb.toString(); - } - - @Override - public String valueToTagString(int maxDepth) { - StringBuilder sb = new StringBuilder("["); - for (int i = 0; i < size(); i++) { - sb.append(i > 0 ? "," : "").append(get(i).valueToTagString(decrementMaxDepth(maxDepth))); - } - sb.append("]"); - return sb.toString(); - } - - @Override - public boolean equals(Object other) { - if (!super.equals(other) || size() != ((StructTag) other).size()) { - return false; - } - for (int i = 0; i < size(); i++) { - if (!get(i).equals(((StructTag) other).get(i))) { - return false; - } - } - return true; - } - - @Override - public int hashCode() { - return getValue().hashCode(); - } - - @Override - public int compareTo(StructTag o) { - return Integer.compare(size(), o.size()); - } - - @Override - public StructTag clone() { - StructTag copy = new StructTag(); - for (Tag tag : getValue()) { - copy.add(tag.clone()); - } - return copy; - } -} diff --git a/src/main/java/net/querz/nbt/io/LittleEndianNBTInputStream.java b/src/main/java/net/querz/nbt/io/LittleEndianNBTInputStream.java new file mode 100644 index 00000000..7c7d8ed2 --- /dev/null +++ b/src/main/java/net/querz/nbt/io/LittleEndianNBTInputStream.java @@ -0,0 +1,240 @@ +package net.querz.nbt.io; + +import net.querz.io.ExceptionBiFunction; +import net.querz.io.MaxDepthIO; +import net.querz.nbt.tag.ByteArrayTag; +import net.querz.nbt.tag.ByteTag; +import net.querz.nbt.tag.CompoundTag; +import net.querz.nbt.tag.DoubleTag; +import net.querz.nbt.tag.EndTag; +import net.querz.nbt.tag.FloatTag; +import net.querz.nbt.tag.IntArrayTag; +import net.querz.nbt.tag.IntTag; +import net.querz.nbt.tag.ListTag; +import net.querz.nbt.tag.LongArrayTag; +import net.querz.nbt.tag.LongTag; +import net.querz.nbt.tag.ShortTag; +import net.querz.nbt.tag.StringTag; +import net.querz.nbt.tag.Tag; +import java.io.Closeable; +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +public class LittleEndianNBTInputStream implements DataInput, NBTInput, MaxDepthIO, Closeable { + + private final DataInputStream input; + + private static Map, IOException>> readers = new HashMap<>(); + private static Map> idClassMapping = new HashMap<>(); + + static { + put(EndTag.ID, (i, d) -> EndTag.INSTANCE, EndTag.class); + put(ByteTag.ID, (i, d) -> readByte(i), ByteTag.class); + put(ShortTag.ID, (i, d) -> readShort(i), ShortTag.class); + put(IntTag.ID, (i, d) -> readInt(i), IntTag.class); + put(LongTag.ID, (i, d) -> readLong(i), LongTag.class); + put(FloatTag.ID, (i, d) -> readFloat(i), FloatTag.class); + put(DoubleTag.ID, (i, d) -> readDouble(i), DoubleTag.class); + put(ByteArrayTag.ID, (i, d) -> readByteArray(i), ByteArrayTag.class); + put(StringTag.ID, (i, d) -> readString(i), StringTag.class); + put(ListTag.ID, LittleEndianNBTInputStream::readListTag, ListTag.class); + put(CompoundTag.ID, LittleEndianNBTInputStream::readCompound, CompoundTag.class); + put(IntArrayTag.ID, (i, d) -> readIntArray(i), IntArrayTag.class); + put(LongArrayTag.ID, (i, d) -> readLongArray(i), LongArrayTag.class); + } + + private static void put(byte id, ExceptionBiFunction, IOException> reader, Class clazz) { + readers.put(id, reader); + idClassMapping.put(id, clazz); + } + + public LittleEndianNBTInputStream(InputStream in) { + input = new DataInputStream(in); + } + + public LittleEndianNBTInputStream(DataInputStream in) { + input = in; + } + + public NamedTag readTag(int maxDepth) throws IOException { + byte id = readByte(); + return new NamedTag(readUTF(), readTag(id, maxDepth)); + } + + public Tag readRawTag(int maxDepth) throws IOException { + byte id = readByte(); + return readTag(id, maxDepth); + } + + private Tag readTag(byte type, int maxDepth) throws IOException { + ExceptionBiFunction, IOException> f; + if ((f = readers.get(type)) == null) { + throw new IOException("invalid tag id \"" + type + "\""); + } + return f.accept(this, maxDepth); + } + + private static ByteTag readByte(LittleEndianNBTInputStream in) throws IOException { + return new ByteTag(in.readByte()); + } + + private static ShortTag readShort(LittleEndianNBTInputStream in) throws IOException { + return new ShortTag(in.readShort()); + } + + private static IntTag readInt(LittleEndianNBTInputStream in) throws IOException { + return new IntTag(in.readInt()); + } + + private static LongTag readLong(LittleEndianNBTInputStream in) throws IOException { + return new LongTag(in.readLong()); + } + + private static FloatTag readFloat(LittleEndianNBTInputStream in) throws IOException { + return new FloatTag(in.readFloat()); + } + + private static DoubleTag readDouble(LittleEndianNBTInputStream in) throws IOException { + return new DoubleTag(in.readDouble()); + } + + private static StringTag readString(LittleEndianNBTInputStream in) throws IOException { + return new StringTag(in.readUTF()); + } + + private static ByteArrayTag readByteArray(LittleEndianNBTInputStream in) throws IOException { + ByteArrayTag bat = new ByteArrayTag(new byte[in.readInt()]); + in.readFully(bat.getValue()); + return bat; + } + + private static IntArrayTag readIntArray(LittleEndianNBTInputStream in) throws IOException { + int l = in.readInt(); + int[] data = new int[l]; + IntArrayTag iat = new IntArrayTag(data); + for (int i = 0; i < l; i++) { + data[i] = in.readInt(); + } + return iat; + } + + private static LongArrayTag readLongArray(LittleEndianNBTInputStream in) throws IOException { + int l = in.readInt(); + long[] data = new long[l]; + LongArrayTag iat = new LongArrayTag(data); + for (int i = 0; i < l; i++) { + data[i] = in.readLong(); + } + return iat; + } + + private static ListTag readListTag(LittleEndianNBTInputStream in, int maxDepth) throws IOException { + byte listType = in.readByte(); + ListTag list = ListTag.createUnchecked(idClassMapping.get(listType)); + int length = in.readInt(); + if (length < 0) { + length = 0; + } + for (int i = 0; i < length; i++) { + list.addUnchecked(in.readTag(listType, in.decrementMaxDepth(maxDepth))); + } + return list; + } + + private static CompoundTag readCompound(LittleEndianNBTInputStream in, int maxDepth) throws IOException { + CompoundTag comp = new CompoundTag(); + for (int id = in.readByte() & 0xFF; id != 0; id = in.readByte() & 0xFF) { + String key = in.readUTF(); + Tag element = in.readTag((byte) id, in.decrementMaxDepth(maxDepth)); + comp.put(key, element); + } + return comp; + } + + @Override + public void readFully(byte[] b) throws IOException { + input.readFully(b); + } + + @Override + public void readFully(byte[] b, int off, int len) throws IOException { + input.readFully(b, off, len); + } + + @Override + public int skipBytes(int n) throws IOException { + return input.skipBytes(n); + } + + @Override + public boolean readBoolean() throws IOException { + return input.readBoolean(); + } + + @Override + public byte readByte() throws IOException { + return input.readByte(); + } + + @Override + public int readUnsignedByte() throws IOException { + return input.readUnsignedByte(); + } + + @Override + public short readShort() throws IOException { + return Short.reverseBytes(input.readShort()); + } + + public int readUnsignedShort() throws IOException { + return Short.toUnsignedInt(Short.reverseBytes(input.readShort())); + } + + @Override + public char readChar() throws IOException { + return Character.reverseBytes(input.readChar()); + } + + @Override + public int readInt() throws IOException { + return Integer.reverseBytes(input.readInt()); + } + + @Override + public long readLong() throws IOException { + return Long.reverseBytes(input.readLong()); + } + + @Override + public float readFloat() throws IOException { + return Float.intBitsToFloat(Integer.reverseBytes(input.readInt())); + } + + @Override + public double readDouble() throws IOException { + return Double.longBitsToDouble(Long.reverseBytes(input.readLong())); + } + + @Override + @Deprecated + public String readLine() throws IOException { + return input.readLine(); + } + + @Override + public void close() throws IOException { + input.close(); + } + + @Override + public String readUTF() throws IOException { + byte[] bytes = new byte[readUnsignedShort()]; + readFully(bytes); + return new String(bytes, StandardCharsets.UTF_8); + } +} diff --git a/src/main/java/net/querz/nbt/io/LittleEndianNBTOutputStream.java b/src/main/java/net/querz/nbt/io/LittleEndianNBTOutputStream.java new file mode 100644 index 00000000..9cda01b2 --- /dev/null +++ b/src/main/java/net/querz/nbt/io/LittleEndianNBTOutputStream.java @@ -0,0 +1,244 @@ +package net.querz.nbt.io; + +import net.querz.io.ExceptionTriConsumer; +import net.querz.io.MaxDepthIO; +import net.querz.nbt.tag.ByteArrayTag; +import net.querz.nbt.tag.ByteTag; +import net.querz.nbt.tag.CompoundTag; +import net.querz.nbt.tag.DoubleTag; +import net.querz.nbt.tag.EndTag; +import net.querz.nbt.tag.FloatTag; +import net.querz.nbt.tag.IntArrayTag; +import net.querz.nbt.tag.IntTag; +import net.querz.nbt.tag.ListTag; +import net.querz.nbt.tag.LongArrayTag; +import net.querz.nbt.tag.LongTag; +import net.querz.nbt.tag.ShortTag; +import net.querz.nbt.tag.StringTag; +import net.querz.nbt.tag.Tag; +import java.io.Closeable; +import java.io.DataOutput; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +public class LittleEndianNBTOutputStream implements DataOutput, NBTOutput, MaxDepthIO, Closeable { + + private final DataOutputStream output; + + private static Map, Integer, IOException>> writers = new HashMap<>(); + private static Map, Byte> classIdMapping = new HashMap<>(); + + static { + put(EndTag.ID, (o, t, d) -> {}, EndTag.class); + put(ByteTag.ID, (o, t, d) -> writeByte(o, t), ByteTag.class); + put(ShortTag.ID, (o, t, d) -> writeShort(o, t), ShortTag.class); + put(IntTag.ID, (o, t, d) -> writeInt(o, t), IntTag.class); + put(LongTag.ID, (o, t, d) -> writeLong(o, t), LongTag.class); + put(FloatTag.ID, (o, t, d) -> writeFloat(o, t), FloatTag.class); + put(DoubleTag.ID, (o, t, d) -> writeDouble(o, t), DoubleTag.class); + put(ByteArrayTag.ID, (o, t, d) -> writeByteArray(o, t), ByteArrayTag.class); + put(StringTag.ID, (o, t, d) -> writeString(o, t), StringTag.class); + put(ListTag.ID, LittleEndianNBTOutputStream::writeList, ListTag.class); + put(CompoundTag.ID, LittleEndianNBTOutputStream::writeCompound, CompoundTag.class); + put(IntArrayTag.ID, (o, t, d) -> writeIntArray(o, t), IntArrayTag.class); + put(LongArrayTag.ID, (o, t, d) -> writeLongArray(o, t), LongArrayTag.class); + } + + private static void put(byte id, ExceptionTriConsumer, Integer, IOException> f, Class clazz) { + writers.put(id, f); + classIdMapping.put(clazz, id); + } + + public LittleEndianNBTOutputStream(OutputStream out) { + output = new DataOutputStream(out); + } + + public LittleEndianNBTOutputStream(DataOutputStream out) { + output = out; + } + + public void writeTag(NamedTag tag, int maxDepth) throws IOException { + writeByte(tag.getTag().getID()); + if (tag.getTag().getID() != 0) { + writeUTF(tag.getName() == null ? "" : tag.getName()); + } + writeRawTag(tag.getTag(), maxDepth); + } + + public void writeTag(Tag tag, int maxDepth) throws IOException { + writeByte(tag.getID()); + if (tag.getID() != 0) { + writeUTF(""); + } + writeRawTag(tag, maxDepth); + } + + public void writeRawTag(Tag tag, int maxDepth) throws IOException { + ExceptionTriConsumer, Integer, IOException> f; + if ((f = writers.get(tag.getID())) == null) { + throw new IOException("invalid tag \"" + tag.getID() + "\""); + } + f.accept(this, tag, maxDepth); + } + + static byte idFromClass(Class clazz) { + Byte id = classIdMapping.get(clazz); + if (id == null) { + throw new IllegalArgumentException("unknown Tag class " + clazz.getName()); + } + return id; + } + + private static void writeByte(LittleEndianNBTOutputStream out, Tag tag) throws IOException { + out.writeByte(((ByteTag) tag).asByte()); + } + + private static void writeShort(LittleEndianNBTOutputStream out, Tag tag) throws IOException { + out.writeShort(((ShortTag) tag).asShort()); + } + + private static void writeInt(LittleEndianNBTOutputStream out, Tag tag) throws IOException { + out.writeInt(((IntTag) tag).asInt()); + } + + private static void writeLong(LittleEndianNBTOutputStream out, Tag tag) throws IOException { + out.writeLong(((LongTag) tag).asLong()); + } + + private static void writeFloat(LittleEndianNBTOutputStream out, Tag tag) throws IOException { + out.writeFloat(((FloatTag) tag).asFloat()); + } + + private static void writeDouble(LittleEndianNBTOutputStream out, Tag tag) throws IOException { + out.writeDouble(((DoubleTag) tag).asDouble()); + } + + private static void writeString(LittleEndianNBTOutputStream out, Tag tag) throws IOException { + out.writeUTF(((StringTag) tag).getValue()); + } + + private static void writeByteArray(LittleEndianNBTOutputStream out, Tag tag) throws IOException { + out.writeInt(((ByteArrayTag) tag).length()); + out.write(((ByteArrayTag) tag).getValue()); + } + + private static void writeIntArray(LittleEndianNBTOutputStream out, Tag tag) throws IOException { + out.writeInt(((IntArrayTag) tag).length()); + for (int i : ((IntArrayTag) tag).getValue()) { + out.writeInt(i); + } + } + + private static void writeLongArray(LittleEndianNBTOutputStream out, Tag tag) throws IOException { + out.writeInt(((LongArrayTag) tag).length()); + for (long l : ((LongArrayTag) tag).getValue()) { + out.writeLong(l); + } + } + + private static void writeList(LittleEndianNBTOutputStream out, Tag tag, int maxDepth) throws IOException { + out.writeByte(idFromClass(((ListTag) tag).getTypeClass())); + out.writeInt(((ListTag) tag).size()); + for (Tag t : ((ListTag) tag)) { + out.writeRawTag(t, out.decrementMaxDepth(maxDepth)); + } + } + + private static void writeCompound(LittleEndianNBTOutputStream out, Tag tag, int maxDepth) throws IOException { + for (Map.Entry> entry : (CompoundTag) tag) { + if (entry.getValue().getID() == 0) { + throw new IOException("end tag not allowed"); + } + out.writeByte(entry.getValue().getID()); + out.writeUTF(entry.getKey()); + out.writeRawTag(entry.getValue(), out.decrementMaxDepth(maxDepth)); + } + out.writeByte(0); + } + + @Override + public void close() throws IOException { + output.close(); + } + + @Override + public void flush() throws IOException { + output.flush(); + } + + @Override + public void write(int b) throws IOException { + output.write(b); + } + + @Override + public void write(byte[] b) throws IOException { + output.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + output.write(b, off, len); + } + + @Override + public void writeBoolean(boolean v) throws IOException { + output.writeBoolean(v); + } + + @Override + public void writeByte(int v) throws IOException { + output.writeByte(v); + } + + @Override + public void writeShort(int v) throws IOException { + output.writeShort(Short.reverseBytes((short) v)); + } + + @Override + public void writeChar(int v) throws IOException { + output.writeChar(Character.reverseBytes((char) v)); + } + + @Override + public void writeInt(int v) throws IOException { + output.writeInt(Integer.reverseBytes(v)); + } + + @Override + public void writeLong(long v) throws IOException { + output.writeLong(Long.reverseBytes(v)); + } + + @Override + public void writeFloat(float v) throws IOException { + output.writeInt(Integer.reverseBytes(Float.floatToIntBits(v))); + } + + @Override + public void writeDouble(double v) throws IOException { + output.writeLong(Long.reverseBytes(Double.doubleToLongBits(v))); + } + + @Override + public void writeBytes(String s) throws IOException { + output.writeBytes(s); + } + + @Override + public void writeChars(String s) throws IOException { + output.writeChars(s); + } + + @Override + public void writeUTF(String s) throws IOException { + byte[] bytes = s.getBytes(StandardCharsets.UTF_8); + writeShort(bytes.length); + write(bytes); + } +} diff --git a/src/main/java/net/querz/nbt/io/NBTDeserializer.java b/src/main/java/net/querz/nbt/io/NBTDeserializer.java new file mode 100644 index 00000000..085e37be --- /dev/null +++ b/src/main/java/net/querz/nbt/io/NBTDeserializer.java @@ -0,0 +1,43 @@ +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, littleEndian; + + public NBTDeserializer() { + this(true); + } + + public NBTDeserializer(boolean compressed) { + this.compressed = compressed; + } + + public NBTDeserializer(boolean compressed, boolean littleEndian) { + this.compressed = compressed; + this.littleEndian = littleEndian; + } + + @Override + public NamedTag fromStream(InputStream stream) throws IOException { + NBTInput nbtIn; + InputStream input; + if (compressed) { + input = new GZIPInputStream(stream); + } else { + input = stream; + } + + if (littleEndian) { + nbtIn = new LittleEndianNBTInputStream(input); + } else { + nbtIn = new NBTInputStream(input); + } + return nbtIn.readTag(Tag.DEFAULT_MAX_DEPTH); + } +} diff --git a/src/main/java/net/querz/nbt/io/NBTInput.java b/src/main/java/net/querz/nbt/io/NBTInput.java new file mode 100644 index 00000000..b17c6123 --- /dev/null +++ b/src/main/java/net/querz/nbt/io/NBTInput.java @@ -0,0 +1,11 @@ +package net.querz.nbt.io; + +import net.querz.nbt.tag.Tag; +import java.io.IOException; + +public interface NBTInput { + + NamedTag readTag(int maxDepth) throws IOException; + + Tag readRawTag(int maxDepth) throws IOException; +} diff --git a/src/main/java/net/querz/nbt/io/NBTInputStream.java b/src/main/java/net/querz/nbt/io/NBTInputStream.java new file mode 100644 index 00000000..dd69c3c7 --- /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 NBTInput, 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/NBTOutput.java b/src/main/java/net/querz/nbt/io/NBTOutput.java new file mode 100644 index 00000000..39f6d688 --- /dev/null +++ b/src/main/java/net/querz/nbt/io/NBTOutput.java @@ -0,0 +1,13 @@ +package net.querz.nbt.io; + +import net.querz.nbt.tag.Tag; +import java.io.IOException; + +public interface NBTOutput { + + void writeTag(NamedTag tag, int maxDepth) throws IOException; + + void writeTag(Tag tag, int maxDepth) throws IOException; + + void flush() throws IOException; +} diff --git a/src/main/java/net/querz/nbt/io/NBTOutputStream.java b/src/main/java/net/querz/nbt/io/NBTOutputStream.java new file mode 100644 index 00000000..ae1a16b0 --- /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 NBTOutput, 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 00000000..921d8f47 --- /dev/null +++ b/src/main/java/net/querz/nbt/io/NBTSerializer.java @@ -0,0 +1,44 @@ +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, littleEndian; + + public NBTSerializer() { + this(true); + } + + public NBTSerializer(boolean compressed) { + this.compressed = compressed; + } + + public NBTSerializer(boolean compressed, boolean littleEndian) { + this.compressed = compressed; + this.littleEndian = littleEndian; + } + + @Override + public void toStream(NamedTag object, OutputStream out) throws IOException { + NBTOutput nbtOut; + OutputStream output; + if (compressed) { + output = new GZIPOutputStream(out, true); + } else { + output = out; + } + + if (littleEndian) { + nbtOut = new LittleEndianNBTOutputStream(output); + } else { + nbtOut = new NBTOutputStream(output); + } + nbtOut.writeTag(object, Tag.DEFAULT_MAX_DEPTH); + nbtOut.flush(); + } +} diff --git a/src/main/java/net/querz/nbt/io/NBTUtil.java b/src/main/java/net/querz/nbt/io/NBTUtil.java new file mode 100644 index 00000000..edd0b869 --- /dev/null +++ b/src/main/java/net/querz/nbt/io/NBTUtil.java @@ -0,0 +1,134 @@ +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 void writeLE(NamedTag tag, File file, boolean compressed) throws IOException { + try (FileOutputStream fos = new FileOutputStream(file)) { + new NBTSerializer(compressed, true).toStream(tag, fos); + } + } + + public static void writeLE(NamedTag tag, String file, boolean compressed) throws IOException { + writeLE(tag, new File(file), compressed); + } + + public static void writeLE(NamedTag tag, File file) throws IOException { + writeLE(tag, file, true); + } + + public static void writeLE(NamedTag tag, String file) throws IOException { + writeLE(tag, new File(file), true); + } + + public static void writeLE(Tag tag, File file, boolean compressed) throws IOException { + writeLE(new NamedTag(null, tag), file, compressed); + } + + public static void writeLE(Tag tag, String file, boolean compressed) throws IOException { + writeLE(new NamedTag(null, tag), new File(file), compressed); + } + + public static void writeLE(Tag tag, File file) throws IOException { + writeLE(new NamedTag(null, tag), file, true); + } + + public static void writeLE(Tag tag, String file) throws IOException { + writeLE(new NamedTag(null, tag), new File(file), true); + } + + public static NamedTag read(File file, boolean compressed) throws IOException { + try (FileInputStream fis = new FileInputStream(file)) { + return new NBTDeserializer(compressed).fromStream(fis); + } + } + + 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)); + } + + public static NamedTag readLE(File file, boolean compressed) throws IOException { + try (FileInputStream fis = new FileInputStream(file)) { + return new NBTDeserializer(compressed, true).fromStream(fis); + } + } + + public static NamedTag readLE(String file, boolean compressed) throws IOException { + return readLE(new File(file), compressed); + } + + public static NamedTag readLE(File file) throws IOException { + try (FileInputStream fis = new FileInputStream(file)) { + return new NBTDeserializer(false, true).fromStream(detectDecompression(fis)); + } + } + + public static NamedTag readLE(String file) throws IOException { + return readLE(new File(file)); + } + + private static InputStream detectDecompression(InputStream is) throws IOException { + PushbackInputStream pbis = new PushbackInputStream(is, 2); + int signature = (pbis.read() & 0xFF) + (pbis.read() << 8); + 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 00000000..b1873087 --- /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 00000000..c62e0610 --- /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 00000000..f45c27e1 --- /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 new SNBTParser(bufferedReader.lines().collect(Collectors.joining())).parse(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 00000000..7d8d2dae --- /dev/null +++ b/src/main/java/net/querz/nbt/io/SNBTParser.java @@ -0,0 +1,253 @@ +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; + + public SNBTParser(String string) { + this.ptr = new StringPointer(string); + } + + public Tag parse(int maxDepth, boolean lenient) throws ParseException { + Tag tag = parseAnything(maxDepth); + if (!lenient) { + ptr.skipWhitespace(); + if (ptr.hasNext()) { + throw ptr.parseException("invalid characters after end of snbt"); + } + } + return tag; + } + + public Tag parse(int maxDepth) throws ParseException { + return parse(maxDepth, false); + } + + public Tag parse() throws ParseException { + return parse(Tag.DEFAULT_MAX_DEPTH, false); + } + + public int getReadChars() { + return ptr.getIndex() + 1; + } + + 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 00000000..50ea44a6 --- /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 00000000..d6f43b26 --- /dev/null +++ b/src/main/java/net/querz/nbt/io/SNBTUtil.java @@ -0,0 +1,19 @@ +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); + } + + public static Tag fromSNBT(String string, boolean lenient) throws IOException { + return new SNBTParser(string).parse(Tag.DEFAULT_MAX_DEPTH, lenient); + } +} 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 00000000..c6cffcb7 --- /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 00000000..fe37b20d --- /dev/null +++ b/src/main/java/net/querz/nbt/io/StringPointer.java @@ -0,0 +1,122 @@ +package net.querz.nbt.io; + +public class StringPointer { + + private String value; + private int index; + + public StringPointer(String value) { + this.value = value; + } + + public int getIndex() { + return index; + } + + public int size() { + return value.length(); + } + + 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/ArrayTag.java b/src/main/java/net/querz/nbt/tag/ArrayTag.java similarity index 97% rename from src/main/java/net/querz/nbt/ArrayTag.java rename to src/main/java/net/querz/nbt/tag/ArrayTag.java index b08d9fc8..2842fa63 100644 --- a/src/main/java/net/querz/nbt/ArrayTag.java +++ b/src/main/java/net/querz/nbt/tag/ArrayTag.java @@ -1,4 +1,4 @@ -package net.querz.nbt; +package net.querz.nbt.tag; import java.lang.reflect.Array; diff --git a/src/main/java/net/querz/nbt/ByteArrayTag.java b/src/main/java/net/querz/nbt/tag/ByteArrayTag.java similarity index 57% rename from src/main/java/net/querz/nbt/ByteArrayTag.java rename to src/main/java/net/querz/nbt/tag/ByteArrayTag.java index f73aae56..8fbcf8a3 100644 --- a/src/main/java/net/querz/nbt/ByteArrayTag.java +++ b/src/main/java/net/querz/nbt/tag/ByteArrayTag.java @@ -1,12 +1,10 @@ -package net.querz.nbt; +package net.querz.nbt.tag; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; 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() { @@ -18,21 +16,8 @@ public ByteArrayTag(byte[] value) { } @Override - public void serializeValue(DataOutputStream dos, int maxDepth) throws IOException { - dos.writeInt(length()); - dos.write(getValue()); - } - - @Override - public void deserializeValue(DataInputStream dis, int maxDepth) throws IOException { - int length = dis.readInt(); - setValue(new byte[length]); - dis.readFully(getValue()); - } - - @Override - public String valueToTagString(int maxDepth) { - return arrayToString("B", "b"); + public byte getID() { + return ID; } @Override diff --git a/src/main/java/net/querz/nbt/ByteTag.java b/src/main/java/net/querz/nbt/tag/ByteTag.java similarity index 61% rename from src/main/java/net/querz/nbt/ByteTag.java rename to src/main/java/net/querz/nbt/tag/ByteTag.java index 5d12e558..207cefd2 100644 --- a/src/main/java/net/querz/nbt/ByteTag.java +++ b/src/main/java/net/querz/nbt/tag/ByteTag.java @@ -1,11 +1,8 @@ -package net.querz.nbt; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; +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() { @@ -20,6 +17,11 @@ public ByteTag(boolean value) { super((byte) (value ? 1 : 0)); } + @Override + public byte getID() { + return ID; + } + public boolean asBoolean() { return getValue() > 0; } @@ -28,21 +30,6 @@ public void setValue(byte value) { super.setValue(value); } - @Override - public void serializeValue(DataOutputStream dos, int maxDepth) throws IOException { - dos.writeByte(getValue()); - } - - @Override - public void deserializeValue(DataInputStream dis, int maxDepth) throws IOException { - setValue(dis.readByte()); - } - - @Override - public String valueToTagString(int maxDepth) { - return getValue() + "b"; - } - @Override public boolean equals(Object other) { return super.equals(other) && asByte() == ((ByteTag) other).asByte(); diff --git a/src/main/java/net/querz/nbt/CompoundTag.java b/src/main/java/net/querz/nbt/tag/CompoundTag.java similarity index 82% rename from src/main/java/net/querz/nbt/CompoundTag.java rename to src/main/java/net/querz/nbt/tag/CompoundTag.java index adbf18b1..f34db876 100644 --- a/src/main/java/net/querz/nbt/CompoundTag.java +++ b/src/main/java/net/querz/nbt/tag/CompoundTag.java @@ -1,8 +1,5 @@ -package net.querz.nbt; +package net.querz.nbt.tag; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; @@ -11,12 +8,26 @@ import java.util.Set; import java.util.function.BiConsumer; -public class CompoundTag extends Tag>> implements Iterable>>, Comparable { +import net.querz.io.MaxDepthIO; + +public class CompoundTag extends Tag>> + implements Iterable>>, Comparable, MaxDepthIO { + + public static final byte ID = 10; public CompoundTag() { super(createEmptyValue()); } + public CompoundTag(int initialCapacity) { + super(new HashMap<>(initialCapacity)); + } + + @Override + public byte getID() { + return ID; + } + private static Map> createEmptyValue() { return new HashMap<>(8); } @@ -74,6 +85,14 @@ public Tag get(String key) { return getValue().get(key); } + public NumberTag getNumberTag(String key) { + return (NumberTag) getValue().get(key); + } + + public Number getNumber(String key) { + return getNumberTag(key).getValue(); + } + public ByteTag getByteTag(String key) { return get(key, ByteTag.class); } @@ -181,6 +200,13 @@ public Tag put(String key, Tag tag) { return getValue().put(Objects.requireNonNull(key), Objects.requireNonNull(tag)); } + public Tag putIfNotNull(String key, Tag tag) { + if (tag == null) { + return this; + } + return put(key, tag); + } + public Tag putBoolean(String key, boolean value) { return put(key, new ByteTag(value)); } @@ -225,25 +251,6 @@ public Tag putLongArray(String key, long[] value) { return put(key, new LongArrayTag(value)); } - @Override - public void serializeValue(DataOutputStream dos, int maxDepth) throws IOException { - for (Map.Entry> e : getValue().entrySet()) { - e.getValue().serialize(dos, e.getKey(), decrementMaxDepth(maxDepth)); - } - EndTag.INSTANCE.serialize(dos, maxDepth); - } - - @Override - public void deserializeValue(DataInputStream dis, int maxDepth) throws IOException { - clear(); - for (int id = dis.readByte() & 0xFF; id != 0; id = dis.readByte() & 0xFF) { - Tag tag = TagFactory.fromID(id); - String name = dis.readUTF(); - tag.deserializeValue(dis, decrementMaxDepth(maxDepth)); - put(name, tag); - } - } - @Override public String valueToString(int maxDepth) { StringBuilder sb = new StringBuilder("{"); @@ -258,20 +265,6 @@ public String valueToString(int maxDepth) { return sb.toString(); } - @Override - public String valueToTagString(int maxDepth) { - StringBuilder sb = new StringBuilder("{"); - boolean first = true; - for (Map.Entry> e : getValue().entrySet()) { - sb.append(first ? "" : ",") - .append(escapeString(e.getKey(), true)).append(":") - .append(e.getValue().valueToTagString(decrementMaxDepth(maxDepth))); - first = false; - } - sb.append("}"); - return sb.toString(); - } - @Override public boolean equals(Object other) { if (this == other) { @@ -296,7 +289,8 @@ public int compareTo(CompoundTag o) { @Override public CompoundTag clone() { - CompoundTag copy = new CompoundTag(); + // Choose initial capacity based on default load factor (0.75) so all entries fit in map without resizing + CompoundTag copy = new CompoundTag((int) Math.ceil(getValue().size() / 0.75f)); for (Map.Entry> e : getValue().entrySet()) { copy.put(e.getKey(), e.getValue().clone()); } diff --git a/src/main/java/net/querz/nbt/DoubleTag.java b/src/main/java/net/querz/nbt/tag/DoubleTag.java similarity index 58% rename from src/main/java/net/querz/nbt/DoubleTag.java rename to src/main/java/net/querz/nbt/tag/DoubleTag.java index 5057941a..28d08658 100644 --- a/src/main/java/net/querz/nbt/DoubleTag.java +++ b/src/main/java/net/querz/nbt/tag/DoubleTag.java @@ -1,11 +1,8 @@ -package net.querz.nbt; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; +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() { @@ -16,23 +13,13 @@ public DoubleTag(double value) { super(value); } - public void setValue(double value) { - super.setValue(value); - } - @Override - public void serializeValue(DataOutputStream dos, int maxDepth) throws IOException { - dos.writeDouble(getValue()); + public byte getID() { + return ID; } - @Override - public void deserializeValue(DataInputStream dis, int maxDepth) throws IOException { - setValue(dis.readDouble()); - } - - @Override - public String valueToTagString(int maxDepth) { - return getValue() + "d"; + public void setValue(double value) { + super.setValue(value); } @Override 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 00000000..30b970b8 --- /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/FloatTag.java b/src/main/java/net/querz/nbt/tag/FloatTag.java similarity index 58% rename from src/main/java/net/querz/nbt/FloatTag.java rename to src/main/java/net/querz/nbt/tag/FloatTag.java index ff248e3f..9d79204f 100644 --- a/src/main/java/net/querz/nbt/FloatTag.java +++ b/src/main/java/net/querz/nbt/tag/FloatTag.java @@ -1,11 +1,8 @@ -package net.querz.nbt; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; +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() { @@ -16,23 +13,13 @@ public FloatTag(float value) { super(value); } - public void setValue(float value) { - super.setValue(value); - } - @Override - public void serializeValue(DataOutputStream dos, int maxDepth) throws IOException { - dos.writeFloat(getValue()); + public byte getID() { + return ID; } - @Override - public void deserializeValue(DataInputStream dis, int maxDepth) throws IOException { - setValue(dis.readFloat()); - } - - @Override - public String valueToTagString(int maxDepth) { - return getValue() + "f"; + public void setValue(float value) { + super.setValue(value); } @Override diff --git a/src/main/java/net/querz/nbt/IntArrayTag.java b/src/main/java/net/querz/nbt/tag/IntArrayTag.java similarity index 54% rename from src/main/java/net/querz/nbt/IntArrayTag.java rename to src/main/java/net/querz/nbt/tag/IntArrayTag.java index 694c3192..1799c93c 100644 --- a/src/main/java/net/querz/nbt/IntArrayTag.java +++ b/src/main/java/net/querz/nbt/tag/IntArrayTag.java @@ -1,12 +1,10 @@ -package net.querz.nbt; +package net.querz.nbt.tag; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; 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() { @@ -18,25 +16,8 @@ public IntArrayTag(int[] value) { } @Override - public void serializeValue(DataOutputStream dos, int maxDepth) throws IOException { - dos.writeInt(length()); - for (int i : getValue()) { - dos.writeInt(i); - } - } - - @Override - public void deserializeValue(DataInputStream dis, int maxDepth) throws IOException { - int length = dis.readInt(); - setValue(new int[length]); - for (int i = 0; i < length; i++) { - getValue()[i] = dis.readInt(); - } - } - - @Override - public String valueToTagString(int maxDepth) { - return arrayToString("I", ""); + public byte getID() { + return ID; } @Override diff --git a/src/main/java/net/querz/nbt/IntTag.java b/src/main/java/net/querz/nbt/tag/IntTag.java similarity index 57% rename from src/main/java/net/querz/nbt/IntTag.java rename to src/main/java/net/querz/nbt/tag/IntTag.java index dcd6e8e3..57c1f2ba 100644 --- a/src/main/java/net/querz/nbt/IntTag.java +++ b/src/main/java/net/querz/nbt/tag/IntTag.java @@ -1,11 +1,8 @@ -package net.querz.nbt; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; +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() { @@ -16,23 +13,13 @@ public IntTag(int value) { super(value); } - public void setValue(int value) { - super.setValue(value); - } - @Override - public void serializeValue(DataOutputStream dos, int maxDepth) throws IOException { - dos.writeInt(getValue()); + public byte getID() { + return ID; } - @Override - public void deserializeValue(DataInputStream dis, int maxDepth) throws IOException { - setValue(dis.readInt()); - } - - @Override - public String valueToTagString(int maxDepth) { - return getValue() + ""; + public void setValue(int value) { + super.setValue(value); } @Override diff --git a/src/main/java/net/querz/nbt/ListTag.java b/src/main/java/net/querz/nbt/tag/ListTag.java similarity index 76% rename from src/main/java/net/querz/nbt/ListTag.java rename to src/main/java/net/querz/nbt/tag/ListTag.java index 321c53f4..955ad1a4 100644 --- a/src/main/java/net/querz/nbt/ListTag.java +++ b/src/main/java/net/querz/nbt/tag/ListTag.java @@ -1,8 +1,5 @@ -package net.querz.nbt; +package net.querz.nbt.tag; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; @@ -11,41 +8,64 @@ import java.util.Objects; import java.util.function.Consumer; +import net.querz.io.MaxDepthIO; + /** * ListTag represents a typed List in the nbt structure. - * An empty {@link ListTag} created using {@link ListTag#createUnchecked()} will be of unknown type - * and returns an {@link EndTag}{@code .class} in {@link ListTag#getTypeClass()}. + * An empty {@link ListTag} will be of type {@link EndTag} (unknown type). * The type of an empty untyped {@link ListTag} can be set by using any of the {@code add()} * methods or any of the {@code as...List()} methods. - * */ -public class ListTag> extends Tag> implements Iterable, Comparable> { + */ +public class ListTag> extends Tag> implements Iterable, Comparable>, MaxDepthIO { + + public static final byte ID = 9; private Class typeClass = null; - private ListTag() { - super(createEmptyValue(3)); + private ListTag(int initialCapacity) { + super(createEmptyValue(initialCapacity)); + } + + @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) { + return createUnchecked(typeClass, 3); } - + /** - *

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

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 + * + *

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 */ - protected static ListTag createUnchecked() { - return new ListTag<>(); + public static ListTag createUnchecked(Class typeClass, int initialCapacity) { + ListTag list = new ListTag<>(initialCapacity); + 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 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); } @@ -53,10 +73,20 @@ private static List createEmptyValue(int 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} + * @throws NullPointerException When {@code typeClass} is {@code null} */ public ListTag(Class typeClass) throws IllegalArgumentException, NullPointerException { - super(createEmptyValue(3)); + this(typeClass, 3); + } + + /** + * @param typeClass The exact class of the elements + * @param initialCapacity Initial capacity of list + * @throws IllegalArgumentException When {@code typeClass} is {@link EndTag}{@code .class} + * @throws NullPointerException When {@code typeClass} is {@code null} + */ + public ListTag(Class typeClass, int initialCapacity) throws IllegalArgumentException, NullPointerException { + super(createEmptyValue(initialCapacity)); if (typeClass == EndTag.class) { throw new IllegalArgumentException("cannot create ListTag with EndTag elements"); } @@ -107,15 +137,16 @@ public T set(int index, T 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) { + if (getTypeClass() == EndTag.class) { typeClass = t.getClass(); } else if (typeClass != t.getClass()) { throw new ClassCastException( @@ -124,7 +155,6 @@ public void add(int index, T t) { typeClass.getSimpleName())); } getValue().add(index, t); - } public void addAll(Collection t) { @@ -196,7 +226,6 @@ public int indexOf(T t) { @SuppressWarnings("unchecked") public > ListTag asTypedList(Class type) { checkTypeClass(type); - typeClass = type; return (ListTag) this; } @@ -251,36 +280,6 @@ public ListTag asCompoundTagList() { return asTypedList(CompoundTag.class); } - @Override - public void serializeValue(DataOutputStream dos, int maxDepth) throws IOException { - dos.writeByte(TagFactory.idFromClass(getTypeClass())); - dos.writeInt(size()); - if (size() != 0) { - for (T t : getValue()) { - t.serializeValue(dos, decrementMaxDepth(maxDepth)); - } - } - } - - @SuppressWarnings("unchecked") - @Override - public void deserializeValue(DataInputStream dis, int maxDepth) throws IOException { - int typeID = dis.readByte(); - if (typeID != 0) { - typeClass = TagFactory.classFromID(typeID); - } - int size = dis.readInt(); - size = size < 0 ? 0 : size; - setValue(createEmptyValue(size)); - if (size != 0) { - for (int i = 0; i < size; i++) { - Tag tag = TagFactory.fromID(typeID); - tag.deserializeValue(dis, decrementMaxDepth(maxDepth)); - add((T) tag); - } - } - } - @Override public String valueToString(int maxDepth) { StringBuilder sb = new StringBuilder("{\"type\":\"").append(getTypeClass().getSimpleName()).append("\",\"list\":["); @@ -291,22 +290,13 @@ public String valueToString(int maxDepth) { return sb.toString(); } - @Override - public String valueToTagString(int maxDepth) { - StringBuilder sb = new StringBuilder("["); - for (int i = 0; i < size(); i++) { - sb.append(i > 0 ? "," : "").append(get(i).valueToTagString(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()) { + if (!super.equals(other) || size() != ((ListTag) other).size() || getTypeClass() != ((ListTag) other) + .getTypeClass()) { return false; } for (int i = 0; i < size(); i++) { @@ -330,7 +320,7 @@ public int compareTo(ListTag o) { @SuppressWarnings("unchecked") @Override public ListTag clone() { - ListTag copy = new ListTag<>(); + ListTag copy = new ListTag<>(this.size()); // assure type safety for clone copy.typeClass = typeClass; for (T t : getValue()) { @@ -339,9 +329,10 @@ public ListTag clone() { return copy; } + //TODO: make private @SuppressWarnings("unchecked") - private void addUnchecked(Tag tag) { - if (typeClass != null && typeClass != tag.getClass()) { + public void addUnchecked(Tag tag) { + if (getTypeClass() != EndTag.class && typeClass != tag.getClass()) { throw new IllegalArgumentException(String.format( "cannot add %s to ListTag<%s>", tag.getClass().getSimpleName(), typeClass.getSimpleName())); @@ -350,7 +341,7 @@ private void addUnchecked(Tag tag) { } private void checkTypeClass(Class clazz) { - if (typeClass != null && typeClass != clazz) { + if (getTypeClass() != EndTag.class && typeClass != clazz) { throw new ClassCastException(String.format( "cannot cast ListTag<%s> to ListTag<%s>", typeClass.getSimpleName(), clazz.getSimpleName())); diff --git a/src/main/java/net/querz/nbt/LongArrayTag.java b/src/main/java/net/querz/nbt/tag/LongArrayTag.java similarity index 54% rename from src/main/java/net/querz/nbt/LongArrayTag.java rename to src/main/java/net/querz/nbt/tag/LongArrayTag.java index ec5452d7..e0528dd1 100644 --- a/src/main/java/net/querz/nbt/LongArrayTag.java +++ b/src/main/java/net/querz/nbt/tag/LongArrayTag.java @@ -1,12 +1,10 @@ -package net.querz.nbt; +package net.querz.nbt.tag; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; 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() { @@ -18,25 +16,8 @@ public LongArrayTag(long[] value) { } @Override - public void serializeValue(DataOutputStream dos, int maxDepth) throws IOException { - dos.writeInt(length()); - for (long i : getValue()) { - dos.writeLong(i); - } - } - - @Override - public void deserializeValue(DataInputStream dis, int maxDepth) throws IOException { - int length = dis.readInt(); - setValue(new long[length]); - for (int i = 0; i < length; i++) { - getValue()[i] = dis.readLong(); - } - } - - @Override - public String valueToTagString(int maxDepth) { - return arrayToString("L", "l"); + public byte getID() { + return ID; } @Override diff --git a/src/main/java/net/querz/nbt/LongTag.java b/src/main/java/net/querz/nbt/tag/LongTag.java similarity index 57% rename from src/main/java/net/querz/nbt/LongTag.java rename to src/main/java/net/querz/nbt/tag/LongTag.java index 38a7e9f2..8f40a325 100644 --- a/src/main/java/net/querz/nbt/LongTag.java +++ b/src/main/java/net/querz/nbt/tag/LongTag.java @@ -1,11 +1,8 @@ -package net.querz.nbt; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; +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() { @@ -16,23 +13,13 @@ public LongTag(long value) { super(value); } - public void setValue(long value) { - super.setValue(value); - } - @Override - public void serializeValue(DataOutputStream dos, int maxDepth) throws IOException { - dos.writeLong(getValue()); + public byte getID() { + return ID; } - @Override - public void deserializeValue(DataInputStream dis, int maxDepth) throws IOException { - setValue(dis.readLong()); - } - - @Override - public String valueToTagString(int maxDepth) { - return getValue() + "l"; + public void setValue(long value) { + super.setValue(value); } @Override diff --git a/src/main/java/net/querz/nbt/NonNullEntrySet.java b/src/main/java/net/querz/nbt/tag/NonNullEntrySet.java similarity index 98% rename from src/main/java/net/querz/nbt/NonNullEntrySet.java rename to src/main/java/net/querz/nbt/tag/NonNullEntrySet.java index 32d8614a..e157ba26 100644 --- a/src/main/java/net/querz/nbt/NonNullEntrySet.java +++ b/src/main/java/net/querz/nbt/tag/NonNullEntrySet.java @@ -1,4 +1,4 @@ -package net.querz.nbt; +package net.querz.nbt.tag; import java.util.Collection; import java.util.Iterator; diff --git a/src/main/java/net/querz/nbt/NumberTag.java b/src/main/java/net/querz/nbt/tag/NumberTag.java similarity index 95% rename from src/main/java/net/querz/nbt/NumberTag.java rename to src/main/java/net/querz/nbt/tag/NumberTag.java index d2fd9ec6..48faa024 100644 --- a/src/main/java/net/querz/nbt/NumberTag.java +++ b/src/main/java/net/querz/nbt/tag/NumberTag.java @@ -1,4 +1,4 @@ -package net.querz.nbt; +package net.querz.nbt.tag; public abstract class NumberTag> extends Tag { diff --git a/src/main/java/net/querz/nbt/ShortTag.java b/src/main/java/net/querz/nbt/tag/ShortTag.java similarity index 57% rename from src/main/java/net/querz/nbt/ShortTag.java rename to src/main/java/net/querz/nbt/tag/ShortTag.java index 3534b217..5f434c37 100644 --- a/src/main/java/net/querz/nbt/ShortTag.java +++ b/src/main/java/net/querz/nbt/tag/ShortTag.java @@ -1,11 +1,8 @@ -package net.querz.nbt; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; +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() { @@ -16,23 +13,13 @@ public ShortTag(short value) { super(value); } - public void setValue(short value) { - super.setValue(value); - } - @Override - public void serializeValue(DataOutputStream dos, int maxDepth) throws IOException { - dos.writeShort(getValue()); + public byte getID() { + return ID; } - @Override - public void deserializeValue(DataInputStream dis, int maxDepth) throws IOException { - setValue(dis.readShort()); - } - - @Override - public String valueToTagString(int maxDepth) { - return getValue() + "s"; + public void setValue(short value) { + super.setValue(value); } @Override diff --git a/src/main/java/net/querz/nbt/StringTag.java b/src/main/java/net/querz/nbt/tag/StringTag.java similarity index 63% rename from src/main/java/net/querz/nbt/StringTag.java rename to src/main/java/net/querz/nbt/tag/StringTag.java index 8350f518..0d30c4b6 100644 --- a/src/main/java/net/querz/nbt/StringTag.java +++ b/src/main/java/net/querz/nbt/tag/StringTag.java @@ -1,11 +1,8 @@ -package net.querz.nbt; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; +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() { @@ -16,6 +13,11 @@ public StringTag(String value) { super(value); } + @Override + public byte getID() { + return ID; + } + @Override public String getValue() { return super.getValue(); @@ -26,26 +28,11 @@ public void setValue(String value) { super.setValue(value); } - @Override - public void serializeValue(DataOutputStream dos, int maxDepth) throws IOException { - dos.writeUTF(getValue()); - } - - @Override - public void deserializeValue(DataInputStream dis, int maxDepth) throws IOException { - setValue(dis.readUTF()); - } - @Override public String valueToString(int maxDepth) { return escapeString(getValue(), false); } - @Override - public String valueToTagString(int maxDepth) { - return escapeString(getValue(), true); - } - @Override public boolean equals(Object other) { return super.equals(other) && getValue().equals(((StringTag) other).getValue()); diff --git a/src/main/java/net/querz/nbt/Tag.java b/src/main/java/net/querz/nbt/tag/Tag.java similarity index 55% rename from src/main/java/net/querz/nbt/Tag.java rename to src/main/java/net/querz/nbt/tag/Tag.java index 2a92c042..dd1c8d55 100644 --- a/src/main/java/net/querz/nbt/Tag.java +++ b/src/main/java/net/querz/nbt/tag/Tag.java @@ -1,8 +1,6 @@ -package net.querz.nbt; +package net.querz.nbt.tag; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; +import net.querz.io.MaxDepthReachedException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -20,7 +18,7 @@ * *

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. + * 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.

* @@ -69,9 +67,7 @@ public Tag(T value) { /** * @return This Tag's ID, usually used for serialization and deserialization. * */ - public byte getID() { - return TagFactory.idFromClass(getClass()); - } + public abstract byte getID(); /** * @return The value of this Tag. @@ -99,74 +95,6 @@ protected T checkValue(T value) { return Objects.requireNonNull(value); } - /** - * Calls {@link Tag#serialize(DataOutputStream, String, int)} with an empty name. - * @see Tag#serialize(DataOutputStream, String, int) - * @param dos The DataOutputStream to write to - * @param maxDepth The maximum nesting depth - * @throws IOException If something went wrong during serialization. - * @throws NullPointerException If {@code dos} is {@code null}. - * @throws MaxDepthReachedException If the maximum nesting depth is exceeded. - * */ - public final void serialize(DataOutputStream dos, int maxDepth) throws IOException { - serialize(dos, "", maxDepth); - } - - /** - * Serializes this Tag starting at the gives depth. - * @param dos The DataOutputStream to write to. - * @param name The name of this Tag, if this is the root Tag. - * @param maxDepth The maximum nesting depth - * @throws IOException If something went wrong during serialization. - * @throws NullPointerException If {@code dos} or {@code name} is {@code null}. - * @throws MaxDepthReachedException If the maximum nesting depth is exceeded. - * */ - public final void serialize(DataOutputStream dos, String name, int maxDepth) throws IOException { - dos.writeByte(getID()); - if (getID() != 0) { - dos.writeUTF(name); - } - serializeValue(dos, maxDepth); - } - - /** - * Deserializes this Tag starting at the given depth. - * The name of the root Tag is ignored. - * @param dis The DataInputStream to read from. - * @param maxDepth The maximum nesting depth. - * @throws IOException If something went wrong during deserialization. - * @throws NullPointerException If {@code dis} is {@code null}. - * @throws MaxDepthReachedException If the maximum nesting depth is exceeded. - * @return The deserialized NBT structure. - * */ - public static Tag deserialize(DataInputStream dis, int maxDepth) throws IOException { - int id = dis.readByte() & 0xFF; - Tag tag = TagFactory.fromID(id); - if (id != 0) { - dis.readUTF(); - tag.deserializeValue(dis, maxDepth); - } - return tag; - } - - /** - * Serializes only the value of this Tag. - * @param dos The DataOutputStream to write to. - * @param maxDepth The maximum nesting depth. - * @throws IOException If something went wrong during serialization. - * @throws MaxDepthReachedException If the maximum nesting depth is exceeded. - * */ - public abstract void serializeValue(DataOutputStream dos, int maxDepth) throws IOException; - - /** - * Deserializes only the value of this Tag. - * @param dis The DataInputStream to read from. - * @param maxDepth The maximum nesting depth. - * @throws IOException If something went wrong during deserialization. - * @throws MaxDepthReachedException If the maximum nesting depth is exceeded - * */ - public abstract void deserializeValue(DataInputStream dis, int maxDepth) throws IOException; - /** * Calls {@link Tag#toString(int)} with an initial depth of {@code 0}. * @see Tag#toString(int) @@ -189,39 +117,21 @@ public String toString(int maxDepth) { } /** - * Returns a JSON representation of the value of this Tag. - * @param maxDepth The maximum nesting depth. + * 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 abstract String valueToString(int maxDepth); - - /** - * Calls {@link Tag#toTagString(int)} with {@link #DEFAULT_MAX_DEPTH}. - * @see Tag#toTagString(int) - * @return The JSON-like string representation of this Tag. - * */ - public final String toTagString() { - return toTagString(DEFAULT_MAX_DEPTH); - } - - /** - * Returns a JSON-like representation of the value of this Tag, usually used for Minecraft commands. - * @param maxDepth The maximum nesting depth. - * @return The JSON-like string representation of this Tag. - * @throws MaxDepthReachedException If the maximum nesting depth is exceeded. - * */ - public String toTagString(int maxDepth) { - return valueToTagString(maxDepth); + public String valueToString() { + return valueToString(DEFAULT_MAX_DEPTH); } /** - * Returns a JSON-like representation of the value of this Tag. + * Returns a JSON representation of the value of this Tag. * @param maxDepth The maximum nesting depth. - * @return The JSON-like string representation of the value of this Tag. + * @return The string representation of the value of this Tag. * @throws MaxDepthReachedException If the maximum nesting depth is exceeded. * */ - public abstract String valueToTagString(int maxDepth); + public abstract String valueToString(int maxDepth); /** * Returns whether this Tag and some other Tag are equal. @@ -253,25 +163,6 @@ public int hashCode() { @SuppressWarnings("CloneDoesntDeclareCloneNotSupportedException") public abstract Tag clone(); - /** - * Decrements {@code maxDepth} by {@code 1}. This method has to be used when a tag is - * (de-)serialized and contains nested tags. Their respective methods are then called - * with {@code decrementMaxDepth(maxDepth)} as maximum nesting depth. - * - * @param maxDepth The value to decrement. - * @return The decremented value. - * @throws MaxDepthReachedException If {@code maxDepth == 0}. - * @throws IllegalArgumentException If {@code maxDepth < 0}. - * */ - protected 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; - } - /** * 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()} diff --git a/src/test/java/net/querz/nbt/ExceptionRunnable.java b/src/test/java/net/querz/ExceptionRunnable.java similarity index 82% rename from src/test/java/net/querz/nbt/ExceptionRunnable.java rename to src/test/java/net/querz/ExceptionRunnable.java index 1c1292ce..ebf57da8 100644 --- a/src/test/java/net/querz/nbt/ExceptionRunnable.java +++ b/src/test/java/net/querz/ExceptionRunnable.java @@ -1,4 +1,4 @@ -package net.querz.nbt; +package net.querz; @FunctionalInterface public interface ExceptionRunnable { diff --git a/src/test/java/net/querz/nbt/ExceptionSupplier.java b/src/test/java/net/querz/ExceptionSupplier.java similarity index 82% rename from src/test/java/net/querz/nbt/ExceptionSupplier.java rename to src/test/java/net/querz/ExceptionSupplier.java index ba87ef04..98de04ed 100644 --- a/src/test/java/net/querz/nbt/ExceptionSupplier.java +++ b/src/test/java/net/querz/ExceptionSupplier.java @@ -1,4 +1,4 @@ -package net.querz.nbt; +package net.querz; @FunctionalInterface public interface ExceptionSupplier { diff --git a/src/test/java/net/querz/nbt/NBTTestCase.java b/src/test/java/net/querz/NBTTestCase.java similarity index 95% rename from src/test/java/net/querz/nbt/NBTTestCase.java rename to src/test/java/net/querz/NBTTestCase.java index 52d00aa5..7d412a59 100644 --- a/src/test/java/net/querz/nbt/NBTTestCase.java +++ b/src/test/java/net/querz/NBTTestCase.java @@ -1,6 +1,11 @@ -package net.querz.nbt; +package net.querz; import junit.framework.TestCase; +import net.querz.nbt.io.NBTDeserializer; +import net.querz.nbt.io.NBTSerializer; +import net.querz.nbt.io.NamedTag; +import net.querz.nbt.tag.Tag; + import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -24,17 +29,13 @@ public abstract class NBTTestCase extends TestCase { @Override public void tearDown() throws Exception { super.tearDown(); - TagFactory.unregisterCustomTag(90); - TagFactory.unregisterCustomTag(100); - TagFactory.unregisterCustomTag(110); - TagFactory.unregisterCustomTag(120); -// cleanupTmpDir(); + cleanupTmpDir(); } protected byte[] serialize(Tag tag) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (DataOutputStream dos = new DataOutputStream(baos)) { - tag.serialize(dos, Tag.DEFAULT_MAX_DEPTH); + new NBTSerializer(false).toStream(new NamedTag(null, tag), dos); } catch (IOException ex) { ex.printStackTrace(); fail(ex.getMessage()); @@ -44,7 +45,7 @@ protected byte[] serialize(Tag tag) { protected Tag deserialize(byte[] data) { try (DataInputStream dis = new DataInputStream(new ByteArrayInputStream(data))) { - return Tag.deserialize(dis, Tag.DEFAULT_MAX_DEPTH); + return new NBTDeserializer(false).fromStream(dis).getTag(); } catch (IOException ex) { ex.printStackTrace(); fail(ex.getMessage()); @@ -60,7 +61,7 @@ protected File getResourceFile(String name) { protected Tag deserializeFromFile(String f) { try (DataInputStream dis = new DataInputStream(new FileInputStream(getResourceFile(f)))) { - return Tag.deserialize(dis, Tag.DEFAULT_MAX_DEPTH); + return new NBTDeserializer(false).fromStream(dis).getTag(); } catch (IOException ex) { ex.printStackTrace(); fail(ex.getMessage()); @@ -251,6 +252,7 @@ protected void cleanupTmpDir() { file.delete(); } } + tmpDir.delete(); } protected String calculateFileMD5(File file) { diff --git a/src/test/java/net/querz/nbt/mca/CompressionTypeTest.java b/src/test/java/net/querz/mca/CompressionTypeTest.java similarity index 94% rename from src/test/java/net/querz/nbt/mca/CompressionTypeTest.java rename to src/test/java/net/querz/mca/CompressionTypeTest.java index 24298df7..65f87125 100644 --- a/src/test/java/net/querz/nbt/mca/CompressionTypeTest.java +++ b/src/test/java/net/querz/mca/CompressionTypeTest.java @@ -1,4 +1,4 @@ -package net.querz.nbt.mca; +package net.querz.mca; public class CompressionTypeTest extends MCATestCase { diff --git a/src/test/java/net/querz/nbt/mca/MCAFileTest.java b/src/test/java/net/querz/mca/MCAFileTest.java similarity index 72% rename from src/test/java/net/querz/nbt/mca/MCAFileTest.java rename to src/test/java/net/querz/mca/MCAFileTest.java index 6fe66c51..dc9c130d 100644 --- a/src/test/java/net/querz/nbt/mca/MCAFileTest.java +++ b/src/test/java/net/querz/mca/MCAFileTest.java @@ -1,7 +1,9 @@ -package net.querz.nbt.mca; +package net.querz.mca; -import net.querz.nbt.CompoundTag; -import net.querz.nbt.ListTag; +import net.querz.nbt.tag.CompoundTag; +import net.querz.nbt.tag.EndTag; +import net.querz.nbt.tag.ListTag; +import static net.querz.mca.LoadFlags.*; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; @@ -28,14 +30,14 @@ public void testGetChunkIndex() { } public void testChangeData() { - MCAFile mcaFile = assertThrowsNoException(() -> MCAUtil.readMCAFile(copyResourceToTmp("r.2.2.mca"))); + MCAFile mcaFile = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("r.2.2.mca"))); assertNotNull(mcaFile); mcaFile.setChunk(0, null); File tmpFile = getNewTmpFile("r.2.2.mca"); - Integer x = assertThrowsNoException(() -> MCAUtil.writeMCAFile(mcaFile, tmpFile, true)); + Integer x = assertThrowsNoException(() -> MCAUtil.write(mcaFile, tmpFile, true)); assertNotNull(x); assertEquals(2, x.intValue()); - MCAFile again = assertThrowsNoException(() -> MCAUtil.readMCAFile(tmpFile)); + MCAFile again = assertThrowsNoException(() -> MCAUtil.read(tmpFile)); assertNotNull(again); for (int i = 0; i < 1024; i++) { if (i != 512 && i != 1023) { @@ -47,11 +49,11 @@ public void testChangeData() { } public void testChangeLastUpdate() { - MCAFile from = assertThrowsNoException(() -> MCAUtil.readMCAFile(copyResourceToTmp("r.2.2.mca"))); + MCAFile from = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("r.2.2.mca"))); assertNotNull(from); File tmpFile = getNewTmpFile("r.2.2.mca"); - assertThrowsNoException(() -> MCAUtil.writeMCAFile(from, tmpFile, true)); - MCAFile to = assertThrowsNoException(() -> MCAUtil.readMCAFile(tmpFile)); + assertThrowsNoException(() -> MCAUtil.write(from, tmpFile, true)); + MCAFile to = assertThrowsNoException(() -> MCAUtil.read(tmpFile)); assertNotNull(to); assertFalse(from.getChunk(0).getLastMCAUpdate() == to.getChunk(0).getLastMCAUpdate()); assertFalse(from.getChunk(512).getLastMCAUpdate() == to.getChunk(512).getLastMCAUpdate()); @@ -61,7 +63,7 @@ public void testChangeLastUpdate() { } public void testGetters() { - MCAFile f = assertThrowsNoException(() -> MCAUtil.readMCAFile(copyResourceToTmp("r.2.2.mca"))); + MCAFile f = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("r.2.2.mca"))); assertNotNull(f); assertThrowsRuntimeException(() -> f.getChunk(-1), IndexOutOfBoundsException.class); @@ -89,7 +91,7 @@ public void testGetters() { assertNotNull(f.getChunk(0).getBiomes()); assertNull(f.getChunk(0).getHeightMaps()); assertNull(f.getChunk(0).getCarvingMasks()); - assertEquals(new ListTag<>(CompoundTag.class), f.getChunk(0).getEntities()); + assertEquals(ListTag.createUnchecked(null), f.getChunk(0).getEntities()); assertNull(f.getChunk(0).getTileEntities()); assertNull(f.getChunk(0).getTileTicks()); assertNull(f.getChunk(0).getLiquidTicks()); @@ -180,30 +182,30 @@ public void testSetters() { } public void testGetBiomeAt() { - MCAFile f = assertThrowsNoException(() -> MCAUtil.readMCAFile(copyResourceToTmp("r.2.2.mca"))); + MCAFile f = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("r.2.2.mca"))); assertEquals(21, f.getBiomeAt(1024, 1024)); assertEquals(-1, f.getBiomeAt(1040, 1024)); - f.setChunk(0, 1, Chunk.newChunk()); + f.setChunk(0, 1, Chunk.newChunk(2201)); assertEquals(-1, f.getBiomeAt(1024, 1040)); } public void testSetBiomeAt() { - MCAFile f = assertThrowsNoException(() -> MCAUtil.readMCAFile(copyResourceToTmp("r.2.2.mca"))); + MCAFile f = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("r.2.2.mca")), true); f.setBiomeAt(1024, 1024, 20); assertEquals(20, f.getChunk(64, 64).updateHandle(64, 64).getCompoundTag("Level").getIntArray("Biomes")[0]); f.setBiomeAt(1039, 1039, 47); assertEquals(47, f.getChunk(64, 64).updateHandle(64, 64).getCompoundTag("Level").getIntArray("Biomes")[255]); f.setBiomeAt(1040, 1024, 20); int[] biomes = f.getChunk(65, 64).updateHandle(65, 64).getCompoundTag("Level").getIntArray("Biomes"); - assertEquals(256, biomes.length); - for (int i = 0; i < 256; i++) { - assertTrue(i == 0 ? biomes[i] == 20 : biomes[i] == -1); + assertEquals(1024, biomes.length); + for (int i = 0; i < 1024; i++) { + assertTrue(i % 16 == 0 ? biomes[i] == 20 : biomes[i] == -1); } } public void testCleanupPaletteAndBlockStates() { - MCAFile f = assertThrowsNoException(() -> MCAUtil.readMCAFile(copyResourceToTmp("r.2.2.mca"))); + MCAFile f = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("r.2.2.mca"))); assertThrowsNoRuntimeException(f::cleanupPalettesAndBlockStates); Chunk c = f.getChunk(0, 0); Section s = c.getSection(0); @@ -233,7 +235,7 @@ public void testCleanupPaletteAndBlockStates() { } public void testSetBlockDataAt() { - MCAFile f = assertThrowsNoException(() -> MCAUtil.readMCAFile(copyResourceToTmp("r.2.2.mca"))); + MCAFile f = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("r.2.2.mca"))); Section section = f.getChunk(0, 0).getSection(0); assertEquals(10, section.getPalette().size()); assertEquals(0b0001000100010001000100010001000100010001000100010001000100010001L, section.getBlockStates()[0]); @@ -298,8 +300,27 @@ public void testSetBlockDataAt() { assertEquals(256, sss.get(0).getLongArray("BlockStates").length); } + public void testSetBlockDataAt2527() { + //test "line break" for DataVersion 2527 + MCAFile f = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("r.2.2.mca"))); + Chunk p = f.getChunk(0, 0); + p.setDataVersion(3000); + Section section = f.getChunk(0, 0).getSection(0); + assertEquals(10, section.getPalette().size()); + assertEquals(0b0001000100010001000100010001000100010001000100010001000100010001L, section.getBlockStates()[0]); + f.setBlockStateAt(0, 0, 0, block("minecraft:custom"), false); + assertEquals(11, section.getPalette().size()); + assertEquals(0b0001000100010001000100010001000100010001000100010001000100011010L, section.getBlockStates()[0]); + int y = 1; + for (int i = 12; i <= 17; i++) { + f.setBlockStateAt(0, y++, 0, block("minecraft:" + i), false); + } + assertEquals(17, section.getPalette().size()); + assertEquals(342, section.getBlockStates().length); + } + public void testGetBlockDataAt() { - MCAFile f = assertThrowsNoException(() -> MCAUtil.readMCAFile(copyResourceToTmp("r.2.2.mca"))); + MCAFile f = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("r.2.2.mca"))); assertEquals(block("minecraft:bedrock"), f.getBlockStateAt(0, 0, 0)); assertNull(f.getBlockStateAt(16, 0, 0)); assertEquals(block("minecraft:dirt"), f.getBlockStateAt(0, 62, 0)); @@ -308,12 +329,12 @@ public void testGetBlockDataAt() { } public void testGetChunkStatus() { - MCAFile f = assertThrowsNoException(() -> MCAUtil.readMCAFile(copyResourceToTmp("r.2.2.mca"))); + MCAFile f = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("r.2.2.mca"))); assertEquals("mobs_spawned", f.getChunk(0, 0).getStatus()); } public void testSetChunkStatus() { - MCAFile f = assertThrowsNoException(() -> MCAUtil.readMCAFile(copyResourceToTmp("r.2.2.mca"))); + MCAFile f = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("r.2.2.mca"))); assertThrowsNoRuntimeException(() -> f.getChunk(0, 0).setStatus("base")); assertEquals("base", f.getChunk(0, 0).updateHandle(64, 64).getCompoundTag("Level").getString("Status")); assertNull(f.getChunk(1, 0)); @@ -342,4 +363,92 @@ public void testChunkInvalidDataTag() { } }, IOException.class); } + + private void assertLoadFLag(Object field, long flags, long wantedFlag) { + if((flags & wantedFlag) != 0) { + assertNotNull(String.format("Should not be null. Flags=%08x, Wanted flag=%08x", flags, wantedFlag), field); + } else { + assertNull(String.format("Should be null. Flags=%08x, Wanted flag=%08x", flags, wantedFlag), field); + } + } + + private void assertPartialChunk(Chunk c, long loadFlags) { + assertLoadFLag(c.getBiomes(), loadFlags, BIOMES); + assertLoadFLag(c.getHeightMaps(), loadFlags, HEIGHTMAPS); + assertLoadFLag(c.getEntities(), loadFlags, ENTITIES); + assertLoadFLag(c.getCarvingMasks(), loadFlags, CARVING_MASKS); + assertLoadFLag(c.getLights(), loadFlags, LIGHTS); + assertLoadFLag(c.getPostProcessing(), loadFlags, POST_PROCESSING); + assertLoadFLag(c.getLiquidTicks(), loadFlags, LIQUID_TICKS); + assertLoadFLag(c.getLiquidsToBeTicked(), loadFlags, LIQUIDS_TO_BE_TICKED); + assertLoadFLag(c.getTileTicks(), loadFlags, TILE_TICKS); + assertLoadFLag(c.getTileEntities(), loadFlags, TILE_ENTITIES); + assertLoadFLag(c.getToBeTicked(), loadFlags, TO_BE_TICKED); + assertLoadFLag(c.getSection(0), loadFlags, BLOCK_LIGHTS|BLOCK_STATES|SKY_LIGHT); + if ((loadFlags & (BLOCK_LIGHTS|BLOCK_STATES|SKY_LIGHT)) != 0) { + Section s = c.getSection(0); + assertNotNull(String.format("Section is null. Flags=%08x", loadFlags), s); + assertLoadFLag(s.getBlockStates(), loadFlags, BLOCK_STATES); + assertLoadFLag(s.getBlockLight(), loadFlags, BLOCK_LIGHTS); + assertLoadFLag(s.getSkyLight(), loadFlags, SKY_LIGHT); + } + } + + public void testPartialLoad() { + long[] flags = new long[] { + BIOMES, + HEIGHTMAPS, + ENTITIES, + CARVING_MASKS, + LIGHTS, + POST_PROCESSING, + LIQUID_TICKS, + LIQUIDS_TO_BE_TICKED, + TILE_TICKS, + TILE_ENTITIES, + TO_BE_TICKED, + BLOCK_STATES, + BLOCK_LIGHTS, + SKY_LIGHT + }; + + MCAFile f = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("r.2.2.mca"))); + Chunk c = f.getChunk(0); + c.setCarvingMasks(getSomeCompoundTag()); + c.setEntities(getSomeCompoundTagList()); + c.setLights(getSomeListTagList()); + c.setTileEntities(getSomeCompoundTagList()); + c.setTileTicks(getSomeCompoundTagList()); + c.setLiquidTicks(getSomeCompoundTagList()); + c.setToBeTicked(getSomeListTagList()); + c.setLiquidsToBeTicked(getSomeListTagList()); + c.setHeightMaps(getSomeCompoundTag()); + c.setPostProcessing(getSomeListTagList()); + c.getSection(0).setBlockLight(new byte[2048]); + File tmp = this.getNewTmpFile("r.2.2.mca"); + assertThrowsNoException(() -> MCAUtil.write(f, tmp)); + + for (long flag : flags) { + MCAFile mcaFile = assertThrowsNoException(() -> MCAUtil.read(tmp, flag)); + c = mcaFile.getChunk(0, 0); + assertPartialChunk(c, flag); + assertThrowsException(() -> MCAUtil.write(mcaFile, getNewTmpFile("r.12.34.mca")), UnsupportedOperationException.class); + } + } + + public void test1_15GetBiomeAt() throws IOException { + MCAFile f = assertThrowsNoException(() -> MCAUtil.read(copyResourceToTmp("r.0.0.mca"))); + assertEquals(162, f.getBiomeAt(31, 0, 63)); + assertEquals(4, f.getBiomeAt(16, 0, 48)); + assertEquals(4, f.getBiomeAt(16, 0, 63)); + assertEquals(162, f.getBiomeAt(31, 0, 48)); + assertEquals(162, f.getBiomeAt(31, 100, 63)); + assertEquals(4, f.getBiomeAt(16, 100, 48)); + assertEquals(4, f.getBiomeAt(16, 100, 63)); + assertEquals(162, f.getBiomeAt(31, 100, 48)); + assertEquals(162, f.getBiomeAt(31, 106, 63)); + assertEquals(4, f.getBiomeAt(16, 106, 48)); + assertEquals(4, f.getBiomeAt(16, 106, 63)); + assertEquals(162, f.getBiomeAt(31, 106, 48)); + } } diff --git a/src/test/java/net/querz/nbt/mca/MCATestCase.java b/src/test/java/net/querz/mca/MCATestCase.java similarity index 91% rename from src/test/java/net/querz/nbt/mca/MCATestCase.java rename to src/test/java/net/querz/mca/MCATestCase.java index e2843b78..1c87e4c7 100644 --- a/src/test/java/net/querz/nbt/mca/MCATestCase.java +++ b/src/test/java/net/querz/mca/MCATestCase.java @@ -1,8 +1,8 @@ -package net.querz.nbt.mca; +package net.querz.mca; -import net.querz.nbt.CompoundTag; -import net.querz.nbt.ListTag; -import net.querz.nbt.NBTTestCase; +import net.querz.nbt.tag.CompoundTag; +import net.querz.nbt.tag.ListTag; +import net.querz.NBTTestCase; public abstract class MCATestCase extends NBTTestCase { diff --git a/src/test/java/net/querz/nbt/mca/MCAUtilTest.java b/src/test/java/net/querz/mca/MCAUtilTest.java similarity index 84% rename from src/test/java/net/querz/nbt/mca/MCAUtilTest.java rename to src/test/java/net/querz/mca/MCAUtilTest.java index c4982d10..a63dde4e 100644 --- a/src/test/java/net/querz/nbt/mca/MCAUtilTest.java +++ b/src/test/java/net/querz/mca/MCAUtilTest.java @@ -1,4 +1,4 @@ -package net.querz.nbt.mca; +package net.querz.mca; import java.io.File; @@ -75,17 +75,17 @@ public void testCreateNameFromLocation() { } public void testMakeMyCoverageGreatAgain() { - assertThrowsException(() -> MCAUtil.readMCAFile((String) null), NullPointerException.class); - assertThrowsException(() -> MCAUtil.writeMCAFile(null, (String) null), NullPointerException.class); - assertThrowsException(() -> MCAUtil.writeMCAFile(null, (File) null), NullPointerException.class); - assertThrowsException(() -> MCAUtil.writeMCAFile(null, (String) null, false), NullPointerException.class); - assertThrowsException(() -> MCAUtil.readMCAFile("r.a.b.mca"), IllegalArgumentException.class); + assertThrowsException(() -> MCAUtil.read((String) null), NullPointerException.class); + assertThrowsException(() -> MCAUtil.write(null, (String) null), NullPointerException.class); + assertThrowsException(() -> MCAUtil.write(null, (File) null), NullPointerException.class); + assertThrowsException(() -> MCAUtil.write(null, (String) null, false), NullPointerException.class); + assertThrowsException(() -> MCAUtil.read("r.a.b.mca"), IllegalArgumentException.class); assertThrowsNoException(() -> new MCAFile(0, 0).serialize(null)); // empty MCAFile will not even attempt to write to file // test overwriting file MCAFile m = new MCAFile(0, 0); m.setChunk(0, Chunk.newChunk()); - assertThrowsNoException(() -> MCAUtil.writeMCAFile(m, getTmpFile("r.0.0.mca"), false), true); - assertThrowsNoException(() -> MCAUtil.writeMCAFile(m, getTmpFile("r.0.0.mca"), false), true); + assertThrowsNoException(() -> MCAUtil.write(m, getTmpFile("r.0.0.mca"), false), true); + assertThrowsNoException(() -> MCAUtil.write(m, getTmpFile("r.0.0.mca"), false), true); } } diff --git a/src/test/java/net/querz/nbt/TagFactoryTest.java b/src/test/java/net/querz/nbt/TagFactoryTest.java deleted file mode 100644 index 0fa7aa6e..00000000 --- a/src/test/java/net/querz/nbt/TagFactoryTest.java +++ /dev/null @@ -1,46 +0,0 @@ -package net.querz.nbt; - -import net.querz.nbt.custom.CharTag; - -public class TagFactoryTest extends NBTTestCase { - - public void testFromID() { - assertEquals(EndTag.class, TagFactory.fromID(0).getClass()); - assertEquals(ByteTag.class, TagFactory.fromID(1).getClass()); - assertEquals(ShortTag.class, TagFactory.fromID(2).getClass()); - assertEquals(IntTag.class, TagFactory.fromID(3).getClass()); - assertEquals(LongTag.class, TagFactory.fromID(4).getClass()); - assertEquals(FloatTag.class, TagFactory.fromID(5).getClass()); - assertEquals(DoubleTag.class, TagFactory.fromID(6).getClass()); - assertEquals(ByteArrayTag.class, TagFactory.fromID(7).getClass()); - assertEquals(StringTag.class, TagFactory.fromID(8).getClass()); - assertEquals(ListTag.class, TagFactory.fromID(9).getClass()); - assertEquals(CompoundTag.class, TagFactory.fromID(10).getClass()); - assertEquals(IntArrayTag.class, TagFactory.fromID(11).getClass()); - assertEquals(LongArrayTag.class, TagFactory.fromID(12).getClass()); - assertThrowsRuntimeException(() -> TagFactory.fromID(-1), IllegalArgumentException.class); - } - - public void testClassFromID() { - assertThrowsNoRuntimeException(() -> TagFactory.classFromID(1)); - assertThrowsRuntimeException(() -> TagFactory.classFromID(20), IllegalArgumentException.class); - } - - public void testIDFromClass() { - assertThrowsNoRuntimeException(() -> TagFactory.idFromClass(ByteTag.class)); - assertThrowsRuntimeException(() -> TagFactory.idFromClass(CharTag.class), IllegalArgumentException.class); - } - - public void testRegisterCustomTag() { - assertThrowsRuntimeException(() -> TagFactory.registerCustomTag(-1, CharTag::new, CharTag.class), IllegalArgumentException.class); - assertThrowsRuntimeException(() -> TagFactory.registerCustomTag(12, CharTag::new, CharTag.class), IllegalArgumentException.class); - assertThrowsRuntimeException(() -> TagFactory.registerCustomTag(128, CharTag::new, CharTag.class), IllegalArgumentException.class); - CharTag.register(); - assertThrowsRuntimeException(CharTag::register, IllegalArgumentException.class); - } - - public void testUnregisterCustomTag() { - CharTag.register(); - assertThrowsNoRuntimeException(() -> TagFactory.unregisterCustomTag(new CharTag().getID())); - } -} diff --git a/src/test/java/net/querz/nbt/TagTest.java b/src/test/java/net/querz/nbt/TagTest.java deleted file mode 100644 index 02937fb3..00000000 --- a/src/test/java/net/querz/nbt/TagTest.java +++ /dev/null @@ -1,75 +0,0 @@ -package net.querz.nbt; - -import java.io.ByteArrayInputStream; -import java.io.DataInputStream; -import java.io.File; -import java.io.IOException; -import java.io.PushbackInputStream; -import java.util.LinkedHashMap; -import java.util.zip.GZIPInputStream; - -public class TagTest extends NBTTestCase { - - public void testWriteReadTag() { - CompoundTag t = new CompoundTag(); - invokeSetValue(t, new LinkedHashMap<>()); - t.putByte("byte", Byte.MAX_VALUE); - t.putShort("short", Short.MAX_VALUE); - File file = getNewTmpFile("compressed.dat"); - try { - NBTUtil.writeTag(t, "name", file, true); - } catch (IOException ex) { - fail(ex.getMessage()); - } - - assertEquals("E8F7B55F81FADB8A5657461D9188DE73", calculateFileMD5(file)); - - try { - CompoundTag tt = (CompoundTag) NBTUtil.readTag(file); - assertEquals(t, tt); - } catch (IOException ex) { - fail(ex.getMessage()); - } - } - - public void testApplyDecompression() { - ByteArrayInputStream baisComp = new ByteArrayInputStream(new byte[]{31, -117, 8, 0, 0, 0, 0, 0, 0, 0}); - try (DataInputStream in = new DataInputStream(baisComp)) { - assertTrue(NBTUtil.applyDecompression(in) instanceof GZIPInputStream); - } catch (IOException ex) { - ex.printStackTrace(); - fail(ex.getMessage()); - } - - ByteArrayInputStream baisUncomp = new ByteArrayInputStream(new byte[]{0, 0}); - try (DataInputStream in = new DataInputStream(baisUncomp)) { - assertTrue(NBTUtil.applyDecompression(in) instanceof PushbackInputStream); - } catch (IOException ex) { - ex.printStackTrace(); - fail(ex.getMessage()); - } - } - - public void testMakeMyCoverageGreatAgain() { - assertThrowsException(() -> NBTUtil.readTag((String) null), NullPointerException.class); - assertThrowsException(() -> NBTUtil.writeTag(null, (String) null), NullPointerException.class); - assertThrowsException(() -> NBTUtil.writeTag(null, (File) null), NullPointerException.class); - assertThrowsException(() -> NBTUtil.writeTag(null, (String) null, false), NullPointerException.class); - assertThrowsException(() -> NBTUtil.writeTag(null, (File) null, false), NullPointerException.class); - assertThrowsException(() -> NBTUtil.writeTag(null, null, (String) null), NullPointerException.class); - assertThrowsException(() -> NBTUtil.writeTag(null, (File) null), NullPointerException.class); - assertThrowsException(() -> NBTUtil.writeTag(null, null, (File) null), NullPointerException.class); - assertThrowsException(() -> NBTUtil.writeTag(null, (File) null), NullPointerException.class); - assertThrowsException(() -> NBTUtil.writeTag(null, null, (File) null, false), NullPointerException.class); - assertThrowsException(() -> NBTUtil.writeTag(null, null, (String) null, false), NullPointerException.class); - - CompoundTag dummy = new CompoundTag(); - assertThrowsNoException(() -> NBTUtil.writeTag(dummy, getNewTmpFile("coverage.dat"))); - assertThrowsNoException(() -> NBTUtil.writeTag(dummy, getNewTmpFile("coverage.dat").getAbsolutePath())); - assertThrowsNoException(() -> NBTUtil.writeTag(dummy, getNewTmpFile("coverage.dat"), true)); - assertThrowsNoException(() -> NBTUtil.writeTag(dummy, getNewTmpFile("coverage.dat").getAbsolutePath(), true)); - assertThrowsNoException(() -> NBTUtil.writeTag(dummy, "foo", getNewTmpFile("coverage.dat"))); - assertThrowsNoException(() -> NBTUtil.writeTag(dummy, "foo", getNewTmpFile("coverage.dat").getAbsolutePath())); - assertThrowsNoException(() -> NBTUtil.writeTag(dummy, "foo", getNewTmpFile("coverage.dat").getAbsolutePath(), true)); - } -} diff --git a/src/test/java/net/querz/nbt/custom/CharTagTest.java b/src/test/java/net/querz/nbt/custom/CharTagTest.java deleted file mode 100644 index 9f9c2bf0..00000000 --- a/src/test/java/net/querz/nbt/custom/CharTagTest.java +++ /dev/null @@ -1,47 +0,0 @@ -package net.querz.nbt.custom; - -import net.querz.nbt.NBTTestCase; -import java.util.Arrays; - -public class CharTagTest extends NBTTestCase { - - public void testStringConversion() { - CharTag.register(); - CharTag t = new CharTag('a'); - assertEquals('a', (char) t.getValue()); - assertEquals(110, t.getID()); - assertEquals("a", t.toTagString()); - assertEquals("{\"type\":\"" + t.getClass().getSimpleName() + "\",\"value\":\"a\"}", t.toString()); - } - - public void testEquals() { - CharTag t = new CharTag('a'); - CharTag t2 = new CharTag('a'); - assertTrue(t.equals(t2)); - CharTag t3 = new CharTag('b'); - assertFalse(t.equals(t3)); - } - - public void testClone() { - CharTag t = new CharTag('a'); - CharTag tc = t.clone(); - assertTrue(t.equals(tc)); - assertFalse(t == tc); - } - - public void testSerializeDeserialize() { - CharTag t = new CharTag('a'); - CharTag.register(); - byte[] data = serialize(t); - assertTrue(Arrays.equals(new byte[]{110, 0, 0, 0, 97}, data)); - CharTag tt = (CharTag) deserialize(data); - assertTrue(t.equals(tt)); - } - - public void testCompareTo() { - assertEquals(0, new CharTag('a').compareTo(new CharTag('a'))); - assertTrue(0 < new CharTag('b').compareTo(new CharTag('a'))); - assertTrue(0 > new CharTag('a').compareTo(new CharTag('b'))); - - } -} diff --git a/src/test/java/net/querz/nbt/custom/ObjectTagTest.java b/src/test/java/net/querz/nbt/custom/ObjectTagTest.java deleted file mode 100644 index e069d88c..00000000 --- a/src/test/java/net/querz/nbt/custom/ObjectTagTest.java +++ /dev/null @@ -1,203 +0,0 @@ -package net.querz.nbt.custom; - -import net.querz.nbt.NBTTestCase; -import net.querz.nbt.NBTUtil; -import net.querz.nbt.TagFactory; -import static org.junit.Assert.assertNotEquals; -import java.io.IOException; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Objects; -import java.util.Random; - -public class ObjectTagTest extends NBTTestCase { - - public void testStringConversion() { - ObjectTag.register(); - DummyObject d = new DummyObject(); - ObjectTag o = new ObjectTag<>(d); - assertEquals(90, o.getID()); - assertEquals("{\"type\":\"ObjectTag\",\"value\":\"" + d + "\"}", o.toString()); - assertEquals("\"" + d + "\"", o.toTagString()); - } - - public void testEquals() { - DummyObject d = new DummyObject(); - ObjectTag o = new ObjectTag<>(d); - ObjectTag o2 = new ObjectTag<>(d); - assertTrue(o.equals(o2)); - ObjectTag o3 = new ObjectTag<>(new DummyObject()); - assertFalse(o.equals(o3)); - ObjectTag o4 = new ObjectTag<>(d.clone()); - assertTrue(o.equals(o4)); - } - - public void testHashCode() { - DummyObject d = new DummyObject(); - DummyObject d2 = new DummyObject(); - ObjectTag o = new ObjectTag<>(d); - ObjectTag o2 = new ObjectTag<>(d2); - assertNotEquals(o.hashCode(), o2.hashCode()); - assertEquals(o.hashCode(), o.clone().hashCode()); - assertEquals(0, new ObjectTag().hashCode()); - } - - public void testClone() { - DummyObject d = new DummyObject(); - ObjectTag o = new ObjectTag<>(d); - ObjectTag c = o.clone(); - assertTrue(o.equals(c)); - assertFalse(o == c); - assertFalse(o.getValue() == c.getValue()); - ObjectTag s = new ObjectTag<>("string"); - ObjectTag cs = s.clone(); - assertTrue(s.equals(cs)); - assertFalse(s == cs); - //String is immutable and not cloneable, so it still has the same reference - //noinspection StringEquality - assertTrue(s.getValue() == cs.getValue()); - } - - public void testSerializeDeserialize() { - DummyObject d = new DummyObject(); - ObjectTag o = new ObjectTag<>(d); - ObjectTag.register(); - byte[] data = serialize(o); - ObjectTag oo = ((ObjectTag) deserialize(data)); - assertNotNull(oo); - assertThrowsNoRuntimeException(() -> oo.asTypedObjectTag(AbstractDummyObject.class)); - assertThrowsRuntimeException(() -> oo.asTypedObjectTag(String.class), ClassCastException.class); - ObjectTag ooo = oo.asTypedObjectTag(AbstractDummyObject.class); - assertTrue(o.equals(ooo)); - } - - public void testNullValue() { - ObjectTag n = new ObjectTag<>(); - assertNull(n.getValue()); - assertEquals("{\"type\":\"ObjectTag\",\"value\":null}", n.toString()); - assertEquals("null", n.toTagString()); - } - - public void testNullValueEquals() { - ObjectTag n = new ObjectTag<>(); - ObjectTag n2 = new ObjectTag<>(null); - assertTrue(n.equals(n2)); - ObjectTag n3 = new ObjectTag<>(null); - assertTrue(n.equals(n3)); - } - - public void testNullValueClone() { - ObjectTag n = new ObjectTag<>(); - ObjectTag nc = n.clone(); - assertTrue(n.equals(nc)); - assertFalse(n == nc); - assertTrue(n.getValue() == nc.getValue()); - } - - public void testNullValueSerializeDeserialize() { - ObjectTag n = new ObjectTag<>(); - ObjectTag.register(); - byte[] data = serialize(n); - ObjectTag nn = ((ObjectTag) deserialize(data)); - assertNotNull(nn); - ObjectTag nnn = nn.asTypedObjectTag(AbstractDummyObject.class); - assertTrue(n.equals(nnn)); - } - - public void testCompareTo() { - ObjectTag d = new ObjectTag<>(new DummyObject()); - ObjectTag d2 = new ObjectTag<>(new DummyObject()); - //not comparable - assertEquals(0, d.compareTo(d2)); - - ObjectTag d3 = new ObjectTag<>("abc"); - ObjectTag d4 = new ObjectTag<>("abd"); - assertTrue(0 > d3.compareTo(d4)); - assertTrue(0 < d4.compareTo(d3)); - - ObjectTag d5 = new ObjectTag<>("abc"); - assertEquals(0, d3.compareTo(d5)); - - DummyObject o = new DummyObject(); - ObjectTag d6 = new ObjectTag<>(o); - ObjectTag d7 = new ObjectTag<>(o); - assertEquals(0, d6.compareTo(d7)); - - ObjectTag d8 = new ObjectTag<>(); - assertEquals(1, d8.compareTo(d7)); - assertEquals(-1, d7.compareTo(d8)); - - ObjectTag d9 = new ObjectTag<>(); - assertEquals(0, d8.compareTo(d9)); - - List> l = new ArrayList<>(); - l.add(d); - l.add(d9); - l.add(d2); - l.sort(Comparator.naturalOrder()); - assertEquals(d9, l.get(2)); - } - - public void testUnknownObject() { - TagFactory.registerCustomTag(90, ObjectTag::new, ObjectTag.class); - assertThrowsException(() -> NBTUtil.readTag(getResourceFile("unknown_object_tag.dat")), IOException.class); - } - - public static abstract class AbstractDummyObject implements Serializable { - private static final long serialVersionUID = 1L; - - @Override - public String toString() { - return "AbstractDummyObject"; - } - } - - private static final Random RANDOM = new Random(); - - public static class DummyObject extends AbstractDummyObject implements Cloneable { - private static final long serialVersionUID = 1L; - - public byte a = (byte) RANDOM.nextInt(Byte.MAX_VALUE); - public short b = (short) RANDOM.nextInt(Short.MAX_VALUE); - public int c = RANDOM.nextInt(); - public long d = RANDOM.nextLong(); - public float e = RANDOM.nextFloat(); - public double f = RANDOM.nextDouble(); - - @Override - public DummyObject clone() { - try { - return (DummyObject) super.clone(); - } catch (CloneNotSupportedException ex) { - ex.printStackTrace(); - return null; - } - } - - @Override - public String toString() { - return String.format("%d/%d/%d/%d/%f/%f", a, b, c, d, e, f); - } - - @Override - public boolean equals(Object other) { - if (other instanceof DummyObject) { - DummyObject t = (DummyObject) other; - return t.a == a - && t.b == b - && t.c == c - && t.d == d - && t.e == e - && t.f == f; - } - return false; - } - - @Override - public int hashCode() { - return Objects.hash(a, b, c, d, e, f); - } - } -} diff --git a/src/test/java/net/querz/nbt/custom/ShortArrayTagTest.java b/src/test/java/net/querz/nbt/custom/ShortArrayTagTest.java deleted file mode 100644 index b4a7fd5b..00000000 --- a/src/test/java/net/querz/nbt/custom/ShortArrayTagTest.java +++ /dev/null @@ -1,61 +0,0 @@ -package net.querz.nbt.custom; - -import net.querz.nbt.NBTTestCase; -import java.util.Arrays; -import static org.junit.Assert.assertNotEquals; - -public class ShortArrayTagTest extends NBTTestCase { - - public void testStringConversion() { - ShortArrayTag.register(); - ShortArrayTag t = new ShortArrayTag(new short[]{Short.MIN_VALUE, 0, Short.MAX_VALUE}); - assertTrue(Arrays.equals(new short[]{Short.MIN_VALUE, 0, Short.MAX_VALUE}, t.getValue())); - assertEquals(100, t.getID()); - assertEquals("[S;-32768s,0s,32767s]", t.toTagString()); - assertEquals("{\"type\":\"" + t.getClass().getSimpleName() + "\",\"value\":[-32768,0,32767]}", t.toString()); - } - - public void testEquals() { - ShortArrayTag t = new ShortArrayTag(new short[]{Short.MIN_VALUE, 0, Short.MAX_VALUE}); - ShortArrayTag t2 = new ShortArrayTag(new short[]{Short.MIN_VALUE, 0, Short.MAX_VALUE}); - assertTrue(t.equals(t2)); - ShortArrayTag t3 = new ShortArrayTag(new short[]{Short.MAX_VALUE, 0, Short.MIN_VALUE}); - assertFalse(t.equals(t3)); - } - - public void testHashCode() { - ShortArrayTag t = new ShortArrayTag(new short[]{Short.MIN_VALUE, 0, Short.MAX_VALUE}); - ShortArrayTag t2 = new ShortArrayTag(new short[]{Short.MAX_VALUE, 0, Short.MIN_VALUE}); - assertNotEquals(t.hashCode(), t2.hashCode()); - assertEquals(t.hashCode(), t.clone().hashCode()); - } - - public void testClone() { - ShortArrayTag t = new ShortArrayTag(new short[]{Short.MIN_VALUE, 0, Short.MAX_VALUE}); - ShortArrayTag tc = t.clone(); - assertTrue(t.equals(tc)); - assertFalse(t == tc); - assertFalse(t.getValue() == tc.getValue()); - } - - public void testSerializeDeserialize() { - ShortArrayTag t = new ShortArrayTag(new short[]{Short.MIN_VALUE, 0, Short.MAX_VALUE}); - ShortArrayTag.register(); - byte[] data = serialize(t); - assertTrue(Arrays.equals(new byte[]{100, 0, 0, 0, 0, 0, 3, -128, 0, 0, 0, 127, -1}, data)); - ShortArrayTag tt = (ShortArrayTag) deserialize(data); - assertTrue(t.equals(tt)); - } - - public void testCompareTo() { - ShortArrayTag t = new ShortArrayTag(new short[]{Short.MIN_VALUE, 0, Short.MAX_VALUE}); - ShortArrayTag t2 = new ShortArrayTag(new short[]{Short.MIN_VALUE, 0, Short.MAX_VALUE}); - ShortArrayTag t3 = new ShortArrayTag(new short[]{Short.MAX_VALUE, 0, Short.MIN_VALUE}); - ShortArrayTag t4 = new ShortArrayTag(new short[]{0, Short.MIN_VALUE}); - assertEquals(0, t.compareTo(t2)); - assertEquals(0, t.compareTo(t3)); - assertTrue(0 < t.compareTo(t4)); - assertTrue(0 > t4.compareTo(t)); - assertThrowsRuntimeException(() -> t.compareTo(null), NullPointerException.class); - } -} diff --git a/src/test/java/net/querz/nbt/custom/StructTagTest.java b/src/test/java/net/querz/nbt/custom/StructTagTest.java deleted file mode 100644 index 77c8cfbc..00000000 --- a/src/test/java/net/querz/nbt/custom/StructTagTest.java +++ /dev/null @@ -1,195 +0,0 @@ -package net.querz.nbt.custom; - -import junit.framework.TestCase; -import net.querz.nbt.ByteTag; -import net.querz.nbt.CompoundTag; -import net.querz.nbt.IntTag; -import net.querz.nbt.ListTag; -import net.querz.nbt.LongTag; -import net.querz.nbt.NBTTestCase; -import net.querz.nbt.StringTag; -import net.querz.nbt.Tag; -import java.util.Arrays; -import static org.junit.Assert.assertNotEquals; - -public class StructTagTest extends NBTTestCase { - - private StructTag createStructTag() { - StructTag s = new StructTag(); - s.add(new ByteTag(Byte.MAX_VALUE)); - s.add(new IntTag(Integer.MAX_VALUE)); - return s; - } - - public void testStringConversion() { - StructTag.register(); - StructTag s = createStructTag(); - assertEquals(120, s.getID()); - assertEquals("[127b,2147483647]", s.toTagString()); - assertEquals("{\"type\":\"StructTag\",\"value\":[{\"type\":\"ByteTag\",\"value\":127},{\"type\":\"IntTag\",\"value\":2147483647}]}", s.toString()); - } - - public void testEquals() { - StructTag s = createStructTag(); - - StructTag s2 = new StructTag(); - s2.add(new ByteTag(Byte.MAX_VALUE)); - s2.add(new IntTag(Integer.MAX_VALUE)); - assertTrue(s.equals(s2)); - - StructTag s3 = new StructTag(); - s3.add(new IntTag(Integer.MAX_VALUE)); - s3.add(new ByteTag(Byte.MAX_VALUE)); - assertFalse(s.equals(s3)); - - StructTag s4 = new StructTag(); - s4.add(new ByteTag(Byte.MAX_VALUE)); - assertFalse(s.equals(s4)); - } - - public void testHashCode() { - StructTag s = createStructTag(); - StructTag s2 = createStructTag(); - s2.addInt(123); - assertNotEquals(s.hashCode(), s2.hashCode()); - assertEquals(s.hashCode(), s.clone().hashCode()); - - } - - public void testClone() { - StructTag s = createStructTag(); - StructTag c = s.clone(); - assertTrue(s.equals(c)); - assertFalse(s == c); - assertFalse(invokeGetValue(s) == invokeGetValue(c)); - } - - public void testSerializeDeserialize() { - StructTag s = createStructTag(); - StructTag.register(); - byte[] data = serialize(s); - assertTrue(Arrays.equals(new byte[]{120, 0, 0, 0, 0, 0, 2, 1, 127, 3, 127, -1, -1, -1}, data)); - StructTag ss = (StructTag) deserialize(data); - assertTrue(s.equals(ss)); - } - - public void testCompareTo() { - StructTag st = new StructTag(); - st.addInt(1); - st.addInt(2); - StructTag so = new StructTag(); - so.addInt(3); - so.addInt(4); - assertEquals(0, st.compareTo(so)); - so.addInt(5); - assertEquals(-1, st.compareTo(so)); - so.remove(2); - so.remove(1); - assertEquals(1, st.compareTo(so)); - assertThrowsRuntimeException(() -> st.compareTo(null), NullPointerException.class); - } - - public void testContains() { - StructTag l = new StructTag(); - l.addInt(1); - l.addLong(2); - assertTrue(l.contains(new IntTag(1))); - assertFalse(l.contains(new IntTag(2))); - assertTrue(l.containsAll(Arrays.asList(new IntTag(1), new LongTag(2)))); - assertFalse(l.containsAll(Arrays.asList(new IntTag(1), new IntTag(2)))); - } - - public void testIterator() { - StructTag l = new StructTag(); - l.addInt(1); - l.addLong(2); - for (Tag t : l) { - assertNotNull(t); - } - l.forEach(TestCase::assertNotNull); - } - - public void testSet() { - StructTag l = createStructTag(); - l.set(1, new ByteTag((byte) 5)); - assertEquals(2, l.size()); - assertEquals(5, l.getByte(1)); - assertThrowsRuntimeException(() -> l.set(0, null), NullPointerException.class); - } - - public void testAdd() { - StructTag l = new StructTag(); - l.addBoolean(true); - assertThrowsNoRuntimeException(() -> l.addShort((short) 5)); - assertEquals(2, l.size()); - assertEquals(1, l.getByte(0)); - l.addByte(Byte.MAX_VALUE); - assertEquals(3, l.size()); - assertEquals(Byte.MAX_VALUE, l.getByte(2)); - l.addBoolean(true); - assertEquals(1, l.getByte(3)); - l.addBoolean(false); - assertEquals(0, l.getByte(4)); - assertTrue(l.getBoolean(3)); - assertFalse(l.getBoolean(4)); - l.remove(new ByteTag(Byte.MAX_VALUE)); - assertEquals(4, l.size()); - assertThrowsRuntimeException(() -> l.remove(-1), ArrayIndexOutOfBoundsException.class); - assertThrowsRuntimeException(() -> l.remove(4), IndexOutOfBoundsException.class); - assertEquals(new ByteTag(true), assertThrowsNoRuntimeException(() -> l.remove(2))); - l.clear(); - assertEquals(0, l.size()); - - StructTag s = new StructTag(); - s.addShort(Short.MAX_VALUE); - assertEquals(1, s.size()); - assertEquals(Short.MAX_VALUE, s.getShort(0)); - StructTag i = new StructTag(); - i.addInt(Integer.MAX_VALUE); - assertEquals(1, i.size()); - assertEquals(Integer.MAX_VALUE, i.getInt(0)); - StructTag lo = new StructTag(); - lo.addLong(Long.MAX_VALUE); - assertEquals(1, lo.size()); - assertEquals(Long.MAX_VALUE, lo.getLong(0)); - StructTag f = new StructTag(); - f.addFloat(Float.MAX_VALUE); - assertEquals(1, f.size()); - assertEquals(Float.MAX_VALUE, f.getFloat(0)); - StructTag d = new StructTag(); - d.addDouble(Double.MAX_VALUE); - assertEquals(1, d.size()); - assertEquals(Double.MAX_VALUE, d.getDouble(0)); - StructTag st = new StructTag(); - st.addString("foo"); - assertEquals(1, st.size()); - assertEquals("foo", st.getString(0)); - StructTag ba = new StructTag(); - ba.addByteArray(new byte[] {Byte.MIN_VALUE, 0, Byte.MAX_VALUE}); - assertEquals(1, ba.size()); - assertTrue(Arrays.equals(new byte[] {Byte.MIN_VALUE, 0, Byte.MAX_VALUE}, ba.getByteArray(0))); - StructTag ia = new StructTag(); - ia.addIntArray(new int[] {Integer.MIN_VALUE, 0, Integer.MAX_VALUE}); - assertEquals(1, ia.size()); - assertTrue(Arrays.equals(new int[] {Integer.MIN_VALUE, 0, Integer.MAX_VALUE}, ia.getIntArray(0))); - StructTag la = new StructTag(); - la.addLongArray(new long[] {Long.MIN_VALUE, 0, Long.MAX_VALUE}); - assertEquals(1, la.size()); - assertTrue(Arrays.equals(new long[] {Long.MIN_VALUE, 0, Long.MAX_VALUE}, la.getLongArray(0))); - StructTag co = new StructTag(); - co.add(new CompoundTag()); - assertEquals(1, co.size()); - assertEquals(new CompoundTag(), co.getCompoundTag(0)); - StructTag li = new StructTag(); - li.add(new ListTag<>(IntTag.class)); - assertEquals(1, li.size()); - assertEquals(new ListTag<>(IntTag.class), li.getListTag(0)); - - StructTag t = new StructTag(); - t.add(0, new StringTag("foo")); - t.add(0, new IntTag(Integer.MAX_VALUE)); - assertEquals(2, t.size()); - assertEquals(new IntTag(Integer.MAX_VALUE), t.get(0)); - assertEquals(new StringTag("foo"), t.get(1)); - } -} diff --git a/src/test/java/net/querz/nbt/io/NamedTagTest.java b/src/test/java/net/querz/nbt/io/NamedTagTest.java new file mode 100644 index 00000000..2dd2d11c --- /dev/null +++ b/src/test/java/net/querz/nbt/io/NamedTagTest.java @@ -0,0 +1,25 @@ +package net.querz.nbt.io; + +import net.querz.NBTTestCase; +import net.querz.nbt.tag.ByteTag; +import net.querz.nbt.tag.ShortTag; + +public class NamedTagTest extends NBTTestCase { + + public void testCreate() { + ByteTag t = new ByteTag(); + NamedTag n = new NamedTag("name", t); + assertEquals("name", n.getName()); + assertTrue(n.getTag() == t); + } + + public void testSet() { + ByteTag t = new ByteTag(); + NamedTag n = new NamedTag("name", t); + n.setName("blah"); + assertEquals("blah", n.getName()); + ShortTag s = new ShortTag(); + n.setTag(s); + assertTrue(n.getTag() == s); + } +} diff --git a/src/test/java/net/querz/nbt/io/SNBTParserTest.java b/src/test/java/net/querz/nbt/io/SNBTParserTest.java new file mode 100644 index 00000000..597d2026 --- /dev/null +++ b/src/test/java/net/querz/nbt/io/SNBTParserTest.java @@ -0,0 +1,159 @@ +package net.querz.nbt.io; + +import net.querz.NBTTestCase; +import net.querz.nbt.tag.*; +import java.util.Arrays; + +public class SNBTParserTest extends NBTTestCase { + + public void testParse() { + Tag t = assertThrowsNoException(() -> new SNBTParser("{abc: def, blah: 4b, blubb: \"string\", \"foo\": 2s}").parse()); + assertEquals(CompoundTag.class, t.getClass()); + CompoundTag c = (CompoundTag) t; + assertEquals(4, c.size()); + assertEquals("def", c.getString("abc")); + assertEquals((byte) 4, c.getByte("blah")); + assertEquals("string", c.getString("blubb")); + assertEquals((short) 2, c.getShort("foo")); + assertFalse(c.containsKey("invalid")); + + // ------------------------------------------------- number tags + + Tag tb = assertThrowsNoException(() -> new SNBTParser("16b").parse()); + assertEquals(ByteTag.class, tb.getClass()); + assertEquals((byte) 16, ((ByteTag) tb).asByte()); + + tb = assertThrowsNoException(() -> new SNBTParser("16B").parse()); + assertEquals(ByteTag.class, tb.getClass()); + assertEquals((byte) 16, ((ByteTag) tb).asByte()); + + assertThrowsException((() -> new SNBTParser("-129b").parse()), ParseException.class); + + Tag ts = assertThrowsNoException(() -> new SNBTParser("17s").parse()); + assertEquals(ShortTag.class, ts.getClass()); + assertEquals((short) 17, ((ShortTag) ts).asShort()); + + ts = assertThrowsNoException(() -> new SNBTParser("17S").parse()); + assertEquals(ShortTag.class, ts.getClass()); + assertEquals((short) 17, ((ShortTag) ts).asShort()); + + assertThrowsException((() -> new SNBTParser("-32769s").parse()), ParseException.class); + + Tag ti = assertThrowsNoException(() -> new SNBTParser("18").parse()); + assertEquals(IntTag.class, ti.getClass()); + assertEquals(18, ((IntTag) ti).asInt()); + + assertThrowsException((() -> new SNBTParser("-2147483649").parse()), ParseException.class); + + Tag tl = assertThrowsNoException(() -> new SNBTParser("19l").parse()); + assertEquals(LongTag.class, tl.getClass()); + assertEquals(19L, ((LongTag) tl).asLong()); + + tl = assertThrowsNoException(() -> new SNBTParser("19L").parse()); + assertEquals(LongTag.class, tl.getClass()); + assertEquals(19L, ((LongTag) tl).asLong()); + + assertThrowsException((() -> new SNBTParser("-9223372036854775809l").parse()), ParseException.class); + + Tag tf = assertThrowsNoException(() -> new SNBTParser("20.3f").parse()); + assertEquals(FloatTag.class, tf.getClass()); + assertEquals(20.3f, ((FloatTag) tf).asFloat()); + + tf = assertThrowsNoException(() -> new SNBTParser("20.3F").parse()); + assertEquals(FloatTag.class, tf.getClass()); + assertEquals(20.3f, ((FloatTag) tf).asFloat()); + + Tag td = assertThrowsNoException(() -> new SNBTParser("21.3d").parse()); + assertEquals(DoubleTag.class, td.getClass()); + assertEquals(21.3d, ((DoubleTag) td).asDouble()); + + td = assertThrowsNoException(() -> new SNBTParser("21.3D").parse()); + assertEquals(DoubleTag.class, td.getClass()); + assertEquals(21.3d, ((DoubleTag) td).asDouble()); + + td = assertThrowsNoException(() -> new SNBTParser("21.3").parse()); + assertEquals(DoubleTag.class, td.getClass()); + assertEquals(21.3d, ((DoubleTag) td).asDouble()); + + Tag tbo = assertThrowsNoException(() -> new SNBTParser("true").parse()); + assertEquals(ByteTag.class, tbo.getClass()); + assertEquals((byte) 1, ((ByteTag) tbo).asByte()); + + tbo = assertThrowsNoException(() -> new SNBTParser("false").parse()); + assertEquals(ByteTag.class, tbo.getClass()); + assertEquals((byte) 0, ((ByteTag) tbo).asByte()); + + // ------------------------------------------------- arrays + + Tag ba = assertThrowsNoException(() -> new SNBTParser("[B; -128,0, 127]").parse()); + assertEquals(ByteArrayTag.class, ba.getClass()); + assertEquals(3, ((ByteArrayTag) ba).length()); + assertTrue(Arrays.equals(new byte[]{-128, 0, 127}, ((ByteArrayTag) ba).getValue())); + + Tag ia = assertThrowsNoException(() -> new SNBTParser("[I; -2147483648, 0,2147483647]").parse()); + assertEquals(IntArrayTag.class, ia.getClass()); + assertEquals(3, ((IntArrayTag) ia).length()); + assertTrue(Arrays.equals(new int[]{-2147483648, 0, 2147483647}, ((IntArrayTag) ia).getValue())); + + Tag la = assertThrowsNoException(() -> new SNBTParser("[L; -9223372036854775808, 0, 9223372036854775807 ]").parse()); + assertEquals(LongArrayTag.class, la.getClass()); + assertEquals(3, ((LongArrayTag) la).length()); + assertTrue(Arrays.equals(new long[]{-9223372036854775808L, 0, 9223372036854775807L}, ((LongArrayTag) la).getValue())); + + // ------------------------------------------------- invalid arrays + + assertThrowsException((() -> new SNBTParser("[B; -129]").parse()), ParseException.class); + assertThrowsException((() -> new SNBTParser("[I; -2147483649]").parse()), ParseException.class); + assertThrowsException((() -> new SNBTParser("[L; -9223372036854775809]").parse()), ParseException.class); + assertThrowsException((() -> new SNBTParser("[B; 123b]").parse()), ParseException.class); + assertThrowsException((() -> new SNBTParser("[I; 123i]").parse()), ParseException.class); + assertThrowsException((() -> new SNBTParser("[L; 123l]").parse()), ParseException.class); + assertThrowsException((() -> new SNBTParser("[K; -129]").parse()), ParseException.class); + + // ------------------------------------------------- high level errors + + assertThrowsException(() -> new SNBTParser("{20:10} {blah:blubb}").parse(), ParseException.class); + + // ------------------------------------------------- string tag + + Tag st = assertThrowsNoException(() -> new SNBTParser("abc").parse()); + assertEquals(StringTag.class, st.getClass()); + assertEquals("abc", ((StringTag) st).getValue()); + + st = assertThrowsNoException(() -> new SNBTParser("\"abc\"").parse()); + assertEquals(StringTag.class, st.getClass()); + assertEquals("abc", ((StringTag) st).getValue()); + + st = assertThrowsNoException(() -> new SNBTParser("123a").parse()); + assertEquals(StringTag.class, st.getClass()); + assertEquals("123a", ((StringTag) st).getValue()); + + // ------------------------------------------------- list tag + + Tag lt = assertThrowsNoException(() -> new SNBTParser("[abc, \"def\", \"123\" ]").parse()); + assertEquals(ListTag.class, lt.getClass()); + assertEquals(StringTag.class, ((ListTag) lt).getTypeClass()); + assertEquals(3, ((ListTag) lt).size()); + assertEquals("abc", ((ListTag) lt).asStringTagList().get(0).getValue()); + assertEquals("def", ((ListTag) lt).asStringTagList().get(1).getValue()); + assertEquals("123", ((ListTag) lt).asStringTagList().get(2).getValue()); + + assertThrowsException(() -> new SNBTParser("[123, 456").parse(), ParseException.class); + assertThrowsException(() -> new SNBTParser("[123, 456d]").parse(), ParseException.class); + + // ------------------------------------------------- compound tag + + Tag ct = assertThrowsNoException(() -> new SNBTParser("{abc: def,\"key\": 123d, blah: [L;123, 456], blubb: [123, 456]}").parse()); + assertEquals(CompoundTag.class, ct.getClass()); + assertEquals(4, ((CompoundTag) ct).size()); + assertEquals("def", assertThrowsNoException(() -> ((CompoundTag) ct).getString("abc"))); + assertEquals(123D, assertThrowsNoException(() -> ((CompoundTag) ct).getDouble("key"))); + assertTrue(Arrays.equals(new long[]{123, 456}, assertThrowsNoException(() -> ((CompoundTag) ct).getLongArray("blah")))); + assertEquals(2, assertThrowsNoException(() -> ((CompoundTag) ct).getListTag("blubb")).size()); + assertEquals(IntTag.class, ((CompoundTag) ct).getListTag("blubb").getTypeClass()); + + assertThrowsException(() -> new SNBTParser("{abc: def").parse(), ParseException.class); + assertThrowsException(() -> new SNBTParser("{\"\":empty}").parse(), ParseException.class); + assertThrowsException(() -> new SNBTParser("{empty:}").parse(), ParseException.class); + } +} diff --git a/src/test/java/net/querz/nbt/io/SNBTWriterTest.java b/src/test/java/net/querz/nbt/io/SNBTWriterTest.java new file mode 100644 index 00000000..2553802e --- /dev/null +++ b/src/test/java/net/querz/nbt/io/SNBTWriterTest.java @@ -0,0 +1,57 @@ +package net.querz.nbt.io; + +import net.querz.NBTTestCase; +import net.querz.nbt.tag.*; + +import java.util.LinkedHashMap; + +public class SNBTWriterTest extends NBTTestCase { + + public void testWrite() { + + // write number tags + + assertEquals("127b", assertThrowsNoException(() -> SNBTUtil.toSNBT(new ByteTag(Byte.MAX_VALUE)))); + assertEquals("-32768s", assertThrowsNoException(() -> SNBTUtil.toSNBT(new ShortTag(Short.MIN_VALUE)))); + assertEquals("-2147483648", assertThrowsNoException(() -> SNBTUtil.toSNBT(new IntTag(Integer.MIN_VALUE)))); + assertEquals("-9223372036854775808l", assertThrowsNoException(() -> SNBTUtil.toSNBT(new LongTag(Long.MIN_VALUE)))); + assertEquals("123.456f", assertThrowsNoException(() -> SNBTUtil.toSNBT(new FloatTag(123.456F)))); + assertEquals("123.456d", assertThrowsNoException(() -> SNBTUtil.toSNBT(new DoubleTag(123.456D)))); + + // write array tags + + assertEquals("[B;-128,0,127]", assertThrowsNoException(() -> SNBTUtil.toSNBT(new ByteArrayTag(new byte[]{Byte.MIN_VALUE, 0, Byte.MAX_VALUE})))); + assertEquals("[I;-2147483648,0,2147483647]", assertThrowsNoException(() -> SNBTUtil.toSNBT(new IntArrayTag(new int[]{Integer.MIN_VALUE, 0, Integer.MAX_VALUE})))); + assertEquals("[L;-9223372036854775808,0,9223372036854775807]", assertThrowsNoException(() -> SNBTUtil.toSNBT(new LongArrayTag(new long[]{Long.MIN_VALUE, 0, Long.MAX_VALUE})))); + + // write string tag + + assertEquals("abc", assertThrowsNoException(() -> SNBTUtil.toSNBT(new StringTag("abc")))); + assertEquals("\"123\"", assertThrowsNoException(() -> SNBTUtil.toSNBT(new StringTag("123")))); + assertEquals("\"123.456\"", assertThrowsNoException(() -> SNBTUtil.toSNBT(new StringTag("123.456")))); + assertEquals("\"-123\"", assertThrowsNoException(() -> SNBTUtil.toSNBT(new StringTag("-123")))); + assertEquals("\"-1.23e14\"", assertThrowsNoException(() -> SNBTUtil.toSNBT(new StringTag("-1.23e14")))); + assertEquals("\"äöü\\\\\"", assertThrowsNoException(() -> SNBTUtil.toSNBT(new StringTag("äöü\\")))); + + // write list tag + + ListTag lt = new ListTag<>(StringTag.class); + lt.addString("blah"); + lt.addString("blubb"); + lt.addString("123"); + assertEquals("[blah,blubb,\"123\"]", assertThrowsNoException(() -> SNBTUtil.toSNBT(lt))); + + // write compound tag + CompoundTag ct = new CompoundTag(); + invokeSetValue(ct, new LinkedHashMap<>()); + ct.putString("key", "value"); + ct.putByte("byte", Byte.MAX_VALUE); + ct.putByteArray("array", new byte[]{Byte.MIN_VALUE, 0, Byte.MAX_VALUE}); + ListTag clt = new ListTag<>(StringTag.class); + clt.addString("foo"); + clt.addString("bar"); + ct.put("list", clt); + String ctExpected = "{key:value,byte:127b,array:[B;-128,0,127],list:[foo,bar]}"; + assertEquals(ctExpected, assertThrowsNoException(() -> SNBTUtil.toSNBT(ct))); + } +} diff --git a/src/test/java/net/querz/nbt/io/StringPointerTest.java b/src/test/java/net/querz/nbt/io/StringPointerTest.java new file mode 100644 index 00000000..1030c114 --- /dev/null +++ b/src/test/java/net/querz/nbt/io/StringPointerTest.java @@ -0,0 +1,90 @@ +package net.querz.nbt.io; + +import net.querz.NBTTestCase; + +public class StringPointerTest extends NBTTestCase { + + public void testLookAhead() { + StringPointer ptr = new StringPointer("abcdefg"); + char c = ptr.lookAhead(3); + assertEquals('d', c); + assertEquals('a', ptr.currentChar()); + } + + public void testSkip() { + StringPointer ptr = new StringPointer("abcdefg"); + ptr.skip(3); + assertEquals('d', ptr.currentChar()); + } + + public void testNext() { + StringPointer ptr = new StringPointer("abcdefg"); + assertEquals('a', ptr.next()); + assertEquals('b', ptr.currentChar()); + } + + public void testCurrentChar() { + StringPointer ptr = new StringPointer("abcdefg"); + assertEquals('a', ptr.currentChar()); + ptr.skip(3); + assertEquals('d', ptr.currentChar()); + } + + public void testHasCharsLeft() { + StringPointer ptr = new StringPointer("abcdefg"); + assertTrue(ptr.hasCharsLeft(1)); + assertTrue(ptr.hasCharsLeft(6)); + assertFalse(ptr.hasCharsLeft(7)); + } + + public void testHasNext() { + StringPointer ptr = new StringPointer("abcdefg"); + assertTrue(ptr.hasNext()); + ptr.skip(6); + assertTrue(ptr.hasNext()); + ptr.skip(1); + assertFalse(ptr.hasNext()); + } + + public void testSkipWhitespace() { + StringPointer ptr = new StringPointer("abc \t defg"); + ptr.skip(3); + assertEquals(' ', ptr.currentChar()); + ptr.skipWhitespace(); + assertEquals('d', ptr.currentChar()); + } + + public void testExpectChar() { + StringPointer ptr = new StringPointer("abcdefg"); + assertThrowsNoException(() -> ptr.expectChar('a')); + assertThrowsException(() -> ptr.expectChar('a'), ParseException.class); + } + + public void testNextArrayElement() { + StringPointer ptr = new StringPointer("1, 2, 3"); + ptr.next(); + assertTrue(ptr.nextArrayElement()); + ptr.next(); + assertTrue(ptr.nextArrayElement()); + ptr.next(); + assertFalse(ptr.nextArrayElement()); + } + + public void testParseQuotedString() { + StringPointer ptr = new StringPointer("\"abcdefg\""); + assertEquals("abcdefg", assertThrowsNoException(ptr::parseQuotedString)); + ptr = new StringPointer("\"abc\\def\""); + assertThrowsException(ptr::parseQuotedString, ParseException.class); + ptr = new StringPointer("\"abc\\\\def\\\\\""); + assertEquals("abc\\def\\", assertThrowsNoException(ptr::parseQuotedString)); + ptr = new StringPointer("\"abc"); + assertThrowsException(ptr::parseQuotedString, ParseException.class); + } + + public void testParseSimpleString() { + StringPointer ptr = new StringPointer("abcdefg},{something else}"); + assertEquals("abcdefg", ptr.parseSimpleString()); + ptr = new StringPointer("abcdefg"); + assertEquals("abcdefg", ptr.parseSimpleString()); + } +} diff --git a/src/test/java/net/querz/nbt/ByteArrayTagTest.java b/src/test/java/net/querz/nbt/tag/ByteArrayTagTest.java similarity index 79% rename from src/test/java/net/querz/nbt/ByteArrayTagTest.java rename to src/test/java/net/querz/nbt/tag/ByteArrayTagTest.java index 8de2f590..1d10e033 100644 --- a/src/test/java/net/querz/nbt/ByteArrayTagTest.java +++ b/src/test/java/net/querz/nbt/tag/ByteArrayTagTest.java @@ -1,16 +1,28 @@ -package net.querz.nbt; +package net.querz.nbt.tag; + +import net.querz.NBTTestCase; -import java.io.DataInputStream; -import java.io.DataOutputStream; import java.util.Arrays; public class ByteArrayTagTest extends NBTTestCase { + + public void testCreate() { + ByteArrayTag t = new ByteArrayTag(new byte[]{Byte.MIN_VALUE, 0, Byte.MAX_VALUE}); + assertTrue(Arrays.equals(new byte[]{Byte.MIN_VALUE, 0, Byte.MAX_VALUE}, t.getValue())); + t = new ByteArrayTag(); + assertTrue(Arrays.equals(ByteArrayTag.ZERO_VALUE, t.getValue())); + } + + public void testSetValue() { + ByteArrayTag t = new ByteArrayTag(); + t.setValue(new byte[]{Byte.MIN_VALUE, 0, Byte.MAX_VALUE}); + assertTrue(Arrays.equals(new byte[]{Byte.MIN_VALUE, 0, Byte.MAX_VALUE}, t.getValue())); + } public void testStringConversion() { ByteArrayTag t = new ByteArrayTag(new byte[]{Byte.MIN_VALUE, 0, Byte.MAX_VALUE}); assertTrue(Arrays.equals(new byte[]{Byte.MIN_VALUE, 0, Byte.MAX_VALUE}, t.getValue())); assertEquals(7, t.getID()); - assertEquals("[B;-128b,0b,127b]", t.toTagString()); assertEquals("{\"type\":\"" + t.getClass().getSimpleName() + "\",\"value\":[-128,0,127]}", t.toString()); } @@ -66,18 +78,8 @@ public NotAnArrayTag(String value) { } @Override - public void serializeValue(DataOutputStream dos, int depth) { - throw new UnsupportedOperationException("goddammit, this is a test class, you don't want to save it."); - } - - @Override - public void deserializeValue(DataInputStream dis, int depth) { - throw new UnsupportedOperationException("goddammit, this is a test class, you don't want to load it."); - } - - @Override - public String valueToTagString(int depth) { - return escapeString(getValue(), true); + public byte getID() { + return 0; } @Override diff --git a/src/test/java/net/querz/nbt/ByteTagTest.java b/src/test/java/net/querz/nbt/tag/ByteTagTest.java similarity index 83% rename from src/test/java/net/querz/nbt/ByteTagTest.java rename to src/test/java/net/querz/nbt/tag/ByteTagTest.java index de8b604a..ff9451d0 100644 --- a/src/test/java/net/querz/nbt/ByteTagTest.java +++ b/src/test/java/net/querz/nbt/tag/ByteTagTest.java @@ -1,8 +1,23 @@ -package net.querz.nbt; +package net.querz.nbt.tag; + +import net.querz.NBTTestCase; import java.util.Arrays; public class ByteTagTest extends NBTTestCase { + + public void testCreate() { + ByteTag t = new ByteTag(Byte.MAX_VALUE); + assertEquals(Byte.MAX_VALUE, t.asByte()); + t = new ByteTag(); + assertEquals(ByteTag.ZERO_VALUE, t.asByte()); + } + + public void testSetValue() { + ByteTag t = new ByteTag(); + t.setValue((byte) 123); + assertEquals(123, t.asByte()); + } public void testStringConversion() { ByteTag t = new ByteTag(Byte.MAX_VALUE); @@ -11,7 +26,6 @@ public void testStringConversion() { assertEquals(Byte.MAX_VALUE, t.asInt()); assertEquals(Byte.MAX_VALUE, t.asLong()); assertEquals(1, t.getID()); - assertEquals(Byte.MAX_VALUE + "b", t.toTagString()); assertEquals("{\"type\":\"" + t.getClass().getSimpleName() + "\",\"value\":" + Byte.MAX_VALUE + "}", t.toString()); } diff --git a/src/test/java/net/querz/nbt/CompoundTagTest.java b/src/test/java/net/querz/nbt/tag/CompoundTagTest.java similarity index 88% rename from src/test/java/net/querz/nbt/CompoundTagTest.java rename to src/test/java/net/querz/nbt/tag/CompoundTagTest.java index b511a335..e08426f4 100644 --- a/src/test/java/net/querz/nbt/CompoundTagTest.java +++ b/src/test/java/net/querz/nbt/tag/CompoundTagTest.java @@ -1,8 +1,13 @@ -package net.querz.nbt; +package net.querz.nbt.tag; + +import net.querz.io.MaxDepthReachedException; +import net.querz.NBTTestCase; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.Map; + +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertNotEquals; public class CompoundTagTest extends NBTTestCase { @@ -19,7 +24,6 @@ private CompoundTag createCompoundTag() { public void testStringConversion() { CompoundTag ct = createCompoundTag(); - assertEquals("{b:127b,str:foo,list:[123b]}", ct.toTagString()); assertEquals("{\"type\":\"CompoundTag\"," + "\"value\":{" + "\"b\":{\"type\":\"ByteTag\",\"value\":127}," + @@ -35,16 +39,16 @@ public void testEquals() { ct2.putString("str", "foo"); ct2.put("list", new ListTag<>(ByteTag.class)); ct2.getListTag("list").addByte((byte) 123); - assertTrue(ct.equals(ct2)); + assertEquals(ct, ct2); ct2.getListTag("list").asByteTagList().get(0).setValue((byte) 124); - assertFalse(ct.equals(ct2)); + assertNotEquals(ct, ct2); ct2.remove("str"); - assertFalse(ct.equals(ct2)); + assertNotEquals(ct, ct2); assertThrowsNoRuntimeException(() -> ct.equals("blah")); - assertFalse(ct.equals("blah")); + assertNotEquals("blah", ct); assertEquals(ct, ct); } @@ -135,10 +139,10 @@ public void testHashCode() { public void testClone() { CompoundTag ct = createCompoundTag(); CompoundTag cl = ct.clone(); - assertTrue(ct.equals(cl)); - assertFalse(ct == cl); - assertFalse(ct.get("list") == cl.get("list")); - assertFalse(invokeGetValue(ct) == invokeGetValue(cl)); + assertEquals(ct, cl); + assertNotSame(ct, cl); + assertNotSame(ct.get("list"), cl.get("list")); + assertNotSame(invokeGetValue(ct), invokeGetValue(cl)); } public void testClear() { @@ -152,9 +156,12 @@ public void testClear() { public void testSerializeDeserialize() { CompoundTag ct = createCompoundTag(); byte[] data = serialize(ct); - assertTrue(Arrays.equals(new byte[]{10, 0, 0, 1, 0, 1, 98, 127, 8, 0, 3, 115, 116, 114, 0, 3, 102, 111, 111, 9, 0, 4, 108, 105, 115, 116, 1, 0, 0, 0, 1, 123, 0}, data)); + assertArrayEquals( + new byte[] { 10, 0, 0, 1, 0, 1, 98, 127, 8, 0, 3, 115, 116, 114, 0, 3, 102, 111, 111, 9, 0, 4, 108, 105, 115, 116, + 1, 0, 0, 0, 1, 123, 0 + }, data); CompoundTag tt = (CompoundTag) deserialize(data); - assertTrue(ct.equals(tt)); + assertEquals(ct, tt); } public void testCasting() { @@ -167,9 +174,9 @@ public void testCasting() { assertEquals(Byte.MAX_VALUE, cc.get("b", ByteTag.class).asByte()); assertThrowsRuntimeException(() -> cc.getShort("b"), ClassCastException.class); assertEquals(0, cc.getByte("bb")); - assertEquals(true, cc.getBoolean("b")); + assertTrue(cc.getBoolean("b")); cc.putByte("b2", (byte) 0); - assertEquals(false, cc.getBoolean("b2")); + assertFalse(cc.getBoolean("b2")); cc.putBoolean("b3", false); assertEquals(0, cc.getByte("b3")); cc.putBoolean("b4", true); @@ -220,23 +227,23 @@ public void testCasting() { cc.putByteArray("ba", new byte[]{Byte.MIN_VALUE, 0, Byte.MAX_VALUE}); assertEquals(new ByteArrayTag(new byte[]{Byte.MIN_VALUE, 0, Byte.MAX_VALUE}), cc.getByteArrayTag("ba")); assertNull(cc.getByteArrayTag("baba")); - assertTrue(Arrays.equals(new byte[]{Byte.MIN_VALUE, 0, Byte.MAX_VALUE}, cc.get("ba", ByteArrayTag.class).getValue())); + assertArrayEquals(new byte[] { Byte.MIN_VALUE, 0, Byte.MAX_VALUE }, cc.get("ba", ByteArrayTag.class).getValue()); assertThrowsRuntimeException(() -> cc.getIntArray("ba"), ClassCastException.class); - assertTrue(Arrays.equals(new byte[0], cc.getByteArray("baba"))); + assertArrayEquals(new byte[0], cc.getByteArray("baba")); cc.putIntArray("ia", new int[]{Integer.MIN_VALUE, 0, Integer.MAX_VALUE}); assertEquals(new IntArrayTag(new int[]{Integer.MIN_VALUE, 0, Integer.MAX_VALUE}), cc.getIntArrayTag("ia")); assertNull(cc.getIntArrayTag("iaia")); - assertTrue(Arrays.equals(new int[]{Integer.MIN_VALUE, 0, Integer.MAX_VALUE}, cc.get("ia", IntArrayTag.class).getValue())); + assertArrayEquals(new int[] { Integer.MIN_VALUE, 0, Integer.MAX_VALUE }, cc.get("ia", IntArrayTag.class).getValue()); assertThrowsRuntimeException(() -> cc.getLongArray("ia"), ClassCastException.class); - assertTrue(Arrays.equals(new int[0], cc.getIntArray("iaia"))); + assertArrayEquals(new int[0], cc.getIntArray("iaia")); cc.putLongArray("la", new long[]{Long.MIN_VALUE, 0, Long.MAX_VALUE}); assertEquals(new LongArrayTag(new long[]{Long.MIN_VALUE, 0, Long.MAX_VALUE}), cc.getLongArrayTag("la")); assertNull(cc.getLongArrayTag("lala")); - assertTrue(Arrays.equals(new long[]{Long.MIN_VALUE, 0, Long.MAX_VALUE}, cc.get("la", LongArrayTag.class).getValue())); + assertArrayEquals(new long[] { Long.MIN_VALUE, 0, Long.MAX_VALUE }, cc.get("la", LongArrayTag.class).getValue()); assertThrowsRuntimeException(() -> cc.getListTag("la"), ClassCastException.class); - assertTrue(Arrays.equals(new long[0], cc.getLongArray("lala"))); + assertArrayEquals(new long[0], cc.getLongArray("lala")); cc.put("li", new ListTag<>(IntTag.class)); assertEquals(new ListTag<>(IntTag.class), cc.getListTag("li")); @@ -277,10 +284,7 @@ public void testMaxDepth() { assertThrowsRuntimeException(() -> deserializeFromFile("max_depth_reached.dat"), MaxDepthReachedException.class); assertThrowsNoRuntimeException(() -> root.toString(Tag.DEFAULT_MAX_DEPTH + 1)); assertThrowsRuntimeException(root::toString, MaxDepthReachedException.class); - assertThrowsNoRuntimeException(() -> root.toTagString(Tag.DEFAULT_MAX_DEPTH + 1)); - assertThrowsRuntimeException(root::toTagString, MaxDepthReachedException.class); assertThrowsRuntimeException(() -> root.valueToString(-1), IllegalArgumentException.class); - assertThrowsRuntimeException(() -> root.valueToTagString(-1), IllegalArgumentException.class); } public void testRecursion() { @@ -288,7 +292,6 @@ public void testRecursion() { recursive.put("recursive", recursive); assertThrowsRuntimeException(() -> serialize(recursive), MaxDepthReachedException.class); assertThrowsRuntimeException(recursive::toString, MaxDepthReachedException.class); - assertThrowsRuntimeException(recursive::toTagString, MaxDepthReachedException.class); } public void testEntrySet() { @@ -304,7 +307,7 @@ public void testEntrySet() { public void testContains() { CompoundTag ct = createCompoundTag(); - assertTrue(3 == ct.size()); + assertEquals(3, ct.size()); assertTrue(ct.containsKey("b")); assertTrue(ct.containsKey("str")); assertTrue(ct.containsKey("list")); @@ -361,4 +364,13 @@ public void testIterator() { }); assertEquals(3, ct.size()); } + + public void testPutIfNotNull() { + CompoundTag ct = new CompoundTag(); + assertEquals(0, ct.size()); + ct.putIfNotNull("foo", new StringTag("bar")); + ct.putIfNotNull("bar", null); + assertEquals(1, ct.size()); + assertEquals("bar", ct.getString("foo")); + } } diff --git a/src/test/java/net/querz/nbt/DoubleTagTest.java b/src/test/java/net/querz/nbt/tag/DoubleTagTest.java similarity index 78% rename from src/test/java/net/querz/nbt/DoubleTagTest.java rename to src/test/java/net/querz/nbt/tag/DoubleTagTest.java index 2444aebc..024fca99 100644 --- a/src/test/java/net/querz/nbt/DoubleTagTest.java +++ b/src/test/java/net/querz/nbt/tag/DoubleTagTest.java @@ -1,15 +1,29 @@ -package net.querz.nbt; +package net.querz.nbt.tag; + +import net.querz.NBTTestCase; import static org.junit.Assert.assertNotEquals; import java.util.Arrays; public class DoubleTagTest extends NBTTestCase { + + public void testCreate() { + DoubleTag t = new DoubleTag(Double.MAX_VALUE); + assertEquals(Double.MAX_VALUE, t.asDouble()); + t = new DoubleTag(); + assertEquals(DoubleTag.ZERO_VALUE, t.asDouble()); + } + + public void testSetValue() { + DoubleTag t = new DoubleTag(); + t.setValue(123.4); + assertEquals(123.4, t.asDouble()); + } public void testStringConversion() { DoubleTag t = new DoubleTag(Double.MAX_VALUE); assertEquals(Double.MAX_VALUE, t.asDouble()); assertEquals(6, t.getID()); - assertEquals(Double.MAX_VALUE + "d", t.toTagString()); assertEquals("{\"type\":\"" + t.getClass().getSimpleName() + "\",\"value\":" + Double.MAX_VALUE + "}", t.toString()); } diff --git a/src/test/java/net/querz/nbt/EndTagTest.java b/src/test/java/net/querz/nbt/tag/EndTagTest.java similarity index 53% rename from src/test/java/net/querz/nbt/EndTagTest.java rename to src/test/java/net/querz/nbt/tag/EndTagTest.java index 72c538b5..0044f78d 100644 --- a/src/test/java/net/querz/nbt/EndTagTest.java +++ b/src/test/java/net/querz/nbt/tag/EndTagTest.java @@ -1,4 +1,6 @@ -package net.querz.nbt; +package net.querz.nbt.tag; + +import net.querz.NBTTestCase; public class EndTagTest extends NBTTestCase { @@ -7,15 +9,9 @@ public void testStringConversion() { assertEquals(0, e.getID()); assertNull(e.getValue()); assertEquals("{\"type\":\"" + e.getClass().getSimpleName() + "\",\"value\":\"end\"}", e.toString()); - assertThrowsRuntimeException(e::toTagString, UnsupportedOperationException.class); } public void testClone() { assertTrue(EndTag.INSTANCE == EndTag.INSTANCE.clone()); } - - public void testSerializeDeserialize() { - assertThrowsNoRuntimeException(() -> EndTag.INSTANCE.serializeValue(null, 0)); - assertThrowsNoRuntimeException(() -> EndTag.INSTANCE.deserializeValue(null, 0)); - } } diff --git a/src/test/java/net/querz/nbt/FloatTagTest.java b/src/test/java/net/querz/nbt/tag/FloatTagTest.java similarity index 78% rename from src/test/java/net/querz/nbt/FloatTagTest.java rename to src/test/java/net/querz/nbt/tag/FloatTagTest.java index 1d6e8172..006b40bf 100644 --- a/src/test/java/net/querz/nbt/FloatTagTest.java +++ b/src/test/java/net/querz/nbt/tag/FloatTagTest.java @@ -1,15 +1,29 @@ -package net.querz.nbt; +package net.querz.nbt.tag; + +import net.querz.NBTTestCase; import static org.junit.Assert.assertNotEquals; import java.util.Arrays; public class FloatTagTest extends NBTTestCase { + + public void testCreate() { + FloatTag t = new FloatTag(Float.MAX_VALUE); + assertEquals(Float.MAX_VALUE, t.asFloat()); + t = new FloatTag(); + assertEquals(FloatTag.ZERO_VALUE, t.asFloat()); + } + + public void testSetValue() { + FloatTag t = new FloatTag(); + t.setValue(123.4f); + assertEquals(123.4f, t.asFloat()); + } public void testStringConversion() { FloatTag t = new FloatTag(Float.MAX_VALUE); assertEquals(Float.MAX_VALUE, t.asFloat()); assertEquals(5, t.getID()); - assertEquals(Float.MAX_VALUE + "f", t.toTagString()); assertEquals("{\"type\":\"" + t.getClass().getSimpleName() + "\",\"value\":" + Float.MAX_VALUE + "}", t.toString()); } diff --git a/src/test/java/net/querz/nbt/IntArrayTagTest.java b/src/test/java/net/querz/nbt/tag/IntArrayTagTest.java similarity index 77% rename from src/test/java/net/querz/nbt/IntArrayTagTest.java rename to src/test/java/net/querz/nbt/tag/IntArrayTagTest.java index e02c686e..1458a732 100644 --- a/src/test/java/net/querz/nbt/IntArrayTagTest.java +++ b/src/test/java/net/querz/nbt/tag/IntArrayTagTest.java @@ -1,14 +1,28 @@ -package net.querz.nbt; +package net.querz.nbt.tag; + +import net.querz.NBTTestCase; import java.util.Arrays; public class IntArrayTagTest extends NBTTestCase { + public void testCreate() { + IntArrayTag t = new IntArrayTag(new int[]{Integer.MIN_VALUE, 0, Integer.MAX_VALUE}); + assertTrue(Arrays.equals(new int[]{Integer.MIN_VALUE, 0, Integer.MAX_VALUE}, t.getValue())); + t = new IntArrayTag(); + assertTrue(Arrays.equals(IntArrayTag.ZERO_VALUE, t.getValue())); + } + + public void testSetValue() { + IntArrayTag t = new IntArrayTag(); + t.setValue(new int[]{Integer.MIN_VALUE, 0, Integer.MAX_VALUE}); + assertTrue(Arrays.equals(new int[]{Integer.MIN_VALUE, 0, Integer.MAX_VALUE}, t.getValue())); + } + public void testStringConversion() { IntArrayTag t = new IntArrayTag(new int[]{Integer.MIN_VALUE, 0, Integer.MAX_VALUE}); assertTrue(Arrays.equals(new int[]{Integer.MIN_VALUE, 0, Integer.MAX_VALUE}, t.getValue())); assertEquals(11, t.getID()); - assertEquals("[I;-2147483648,0,2147483647]", t.toTagString()); assertEquals("{\"type\":\"" + t.getClass().getSimpleName() + "\",\"value\":[-2147483648,0,2147483647]}", t.toString()); } diff --git a/src/test/java/net/querz/nbt/IntTagTest.java b/src/test/java/net/querz/nbt/tag/IntTagTest.java similarity index 79% rename from src/test/java/net/querz/nbt/IntTagTest.java rename to src/test/java/net/querz/nbt/tag/IntTagTest.java index d62ad42e..6c15fa42 100644 --- a/src/test/java/net/querz/nbt/IntTagTest.java +++ b/src/test/java/net/querz/nbt/tag/IntTagTest.java @@ -1,15 +1,29 @@ -package net.querz.nbt; +package net.querz.nbt.tag; + +import net.querz.NBTTestCase; import java.util.Arrays; public class IntTagTest extends NBTTestCase { + public void testCreate() { + IntTag t = new IntTag(Integer.MAX_VALUE); + assertEquals(Integer.MAX_VALUE, t.asInt()); + t = new IntTag(); + assertEquals(IntTag.ZERO_VALUE, t.asInt()); + } + + public void testSetValue() { + IntTag t = new IntTag(); + t.setValue(123); + assertEquals(123, t.asInt()); + } + public void testStringConversion() { IntTag t = new IntTag(Integer.MAX_VALUE); assertEquals(Integer.MAX_VALUE, t.asInt()); assertEquals(Integer.MAX_VALUE, t.asLong()); assertEquals(3, t.getID()); - assertEquals(Integer.MAX_VALUE + "", t.toTagString()); assertEquals("{\"type\":\"" + t.getClass().getSimpleName() + "\",\"value\":" + Integer.MAX_VALUE + "}", t.toString()); } diff --git a/src/test/java/net/querz/nbt/ListTagTest.java b/src/test/java/net/querz/nbt/tag/ListTagTest.java similarity index 88% rename from src/test/java/net/querz/nbt/ListTagTest.java rename to src/test/java/net/querz/nbt/tag/ListTagTest.java index 243db1c1..af2b72fb 100644 --- a/src/test/java/net/querz/nbt/ListTagTest.java +++ b/src/test/java/net/querz/nbt/tag/ListTagTest.java @@ -1,6 +1,9 @@ -package net.querz.nbt; +package net.querz.nbt.tag; import junit.framework.TestCase; +import net.querz.io.MaxDepthReachedException; +import net.querz.NBTTestCase; + import java.util.Arrays; import java.util.Comparator; import static org.junit.Assert.assertNotEquals; @@ -26,7 +29,6 @@ public void testStringConversion() { assertEquals(Byte.MIN_VALUE, bl.get(0).asByte()); assertEquals(0, bl.get(1).asByte()); assertEquals(Byte.MAX_VALUE, bl.get(2).asByte()); - assertEquals("[-128b,0b,127b]", bl.toTagString()); assertEquals("{\"type\":\"ListTag\"," + "\"value\":{" + "\"type\":\"ByteTag\"," + @@ -34,7 +36,7 @@ public void testStringConversion() { "-128," + "0," + "127]}}", bl.toString()); - ListTag lu = ListTag.createUnchecked(); + ListTag lu = ListTag.createUnchecked(null); assertEquals("{\"type\":\"ListTag\",\"value\":{\"type\":\"EndTag\",\"list\":[]}}", lu.toString()); } @@ -66,15 +68,15 @@ public void testEquals() { assertEquals(il, new ListTag<>(IntTag.class)); // test empty untyped list - ListTag lu = ListTag.createUnchecked(); - ListTag lu2 = ListTag.createUnchecked(); + ListTag lu = ListTag.createUnchecked(null); + ListTag lu2 = ListTag.createUnchecked(null); assertTrue(lu.equals(lu2)); lu2.asIntTagList(); - assertFalse(lu.equals(lu2)); + assertTrue(lu.equals(lu2)); ListTag lie = new ListTag<>(IntTag.class); assertFalse(lu.equals(lie)); lu.asIntTagList(); - assertTrue(lie.equals(lu)); + assertFalse(lie.equals(lu)); } public void testHashCode() { @@ -89,7 +91,8 @@ public void testClone() { ListTag i = new ListTag<>(IntTag.class); ListTag c = i.clone(); assertThrowsRuntimeException(() -> c.addString("wrong type in clone"), IllegalArgumentException.class); - c.addInt(123); + assertThrowsNoRuntimeException(() -> c.addInt(123)); + assertFalse(i.equals(c)); c.clear(); assertTrue(i.equals(c)); @@ -137,53 +140,53 @@ public void testCasting() { assertEquals(ByteTag.class, b.getTypeClass()); //adjust ListTag type during deserialization - ListTag l = ListTag.createUnchecked(); + ListTag l = ListTag.createUnchecked(null); assertThrowsNoRuntimeException(l::asByteTagList); l.addByte(Byte.MAX_VALUE); assertThrowsNoRuntimeException(l::asByteTagList); assertThrowsRuntimeException(l::asShortTagList, ClassCastException.class); - l = ListTag.createUnchecked(); + l = ListTag.createUnchecked(null); l.addShort(Short.MAX_VALUE); assertThrowsNoRuntimeException(l::asShortTagList); assertThrowsRuntimeException(l::asIntTagList, ClassCastException.class); - l = ListTag.createUnchecked(); + l = ListTag.createUnchecked(null); l.addInt(Integer.MAX_VALUE); assertThrowsNoRuntimeException(l::asIntTagList); assertThrowsRuntimeException(l::asLongTagList, ClassCastException.class); - l = ListTag.createUnchecked(); + l = ListTag.createUnchecked(null); l.addLong(Long.MAX_VALUE); assertThrowsNoRuntimeException(l::asLongTagList); assertThrowsRuntimeException(l::asFloatTagList, ClassCastException.class); - l = ListTag.createUnchecked(); + l = ListTag.createUnchecked(null); l.addFloat(Float.MAX_VALUE); assertThrowsNoRuntimeException(l::asFloatTagList); assertThrowsRuntimeException(l::asDoubleTagList, ClassCastException.class); - l = ListTag.createUnchecked(); + l = ListTag.createUnchecked(null); l.addDouble(Double.MAX_VALUE); assertThrowsNoRuntimeException(l::asDoubleTagList); assertThrowsRuntimeException(l::asStringTagList, ClassCastException.class); - l = ListTag.createUnchecked(); + l = ListTag.createUnchecked(null); l.addString("foo"); assertThrowsNoRuntimeException(l::asStringTagList); assertThrowsRuntimeException(l::asByteArrayTagList, ClassCastException.class); - l = ListTag.createUnchecked(); + l = ListTag.createUnchecked(null); l.addByteArray(new byte[]{Byte.MIN_VALUE, 0, Byte.MAX_VALUE}); assertThrowsNoRuntimeException(l::asByteArrayTagList); assertThrowsRuntimeException(l::asIntArrayTagList, ClassCastException.class); - l = ListTag.createUnchecked(); + l = ListTag.createUnchecked(null); l.addIntArray(new int[]{Integer.MIN_VALUE, 0, Integer.MAX_VALUE}); assertThrowsNoRuntimeException(l::asIntArrayTagList); assertThrowsRuntimeException(l::asLongArrayTagList, ClassCastException.class); - l = ListTag.createUnchecked(); + l = ListTag.createUnchecked(null); l.addLongArray(new long[]{Long.MIN_VALUE, 0, Long.MAX_VALUE}); assertThrowsNoRuntimeException(l::asLongArrayTagList); assertThrowsRuntimeException(l::asListTagList, ClassCastException.class); @@ -198,23 +201,11 @@ public void testCasting() { assertThrowsNoRuntimeException(lco::asCompoundTagList); assertThrowsRuntimeException(lco::asByteTagList, ClassCastException.class); - ListTag lg = ListTag.createUnchecked(); + ListTag lg = ListTag.createUnchecked(null); ListTag lb = assertThrowsNoRuntimeException(lg::asByteTagList); assertEquals(lb, lg); - //only allow casting once from untyped list to typed list - assertThrowsRuntimeException(lg::asShortTagList, ClassCastException.class); - - //adding generic Tags to an empty list - @SuppressWarnings("unchecked") - ListTag> u = (ListTag>) TagFactory.fromID(9); - assertEquals(EndTag.class, u.getTypeClass()); - assertThrowsNoRuntimeException(() -> u.add(new StringTag())); - assertEquals(StringTag.class, u.getTypeClass()); - assertThrowsRuntimeException(() -> u.add(new ByteTag()), ClassCastException.class); - assertThrowsNoRuntimeException(() -> u.add(new StringTag())); - assertEquals(2, u.size()); - - + // allow casting to a different list type if the list is empty + assertThrowsNoException(lg::asShortTagList); } public void testCompareTo() { @@ -244,9 +235,7 @@ public void testMaxDepth() { assertThrowsRuntimeException(() -> serialize(root), MaxDepthReachedException.class); assertThrowsRuntimeException(() -> deserializeFromFile("max_depth_reached.dat"), MaxDepthReachedException.class); assertThrowsRuntimeException(root::toString, MaxDepthReachedException.class); - assertThrowsRuntimeException(root::toTagString, MaxDepthReachedException.class); assertThrowsRuntimeException(() -> root.valueToString(-1), IllegalArgumentException.class); - assertThrowsRuntimeException(() -> root.valueToTagString(-1), IllegalArgumentException.class); } public void testRecursion() { @@ -254,7 +243,6 @@ public void testRecursion() { recursive.add(recursive); assertThrowsRuntimeException(() -> serialize(recursive), MaxDepthReachedException.class); assertThrowsRuntimeException(recursive::toString, MaxDepthReachedException.class); - assertThrowsRuntimeException(recursive::toTagString, MaxDepthReachedException.class); } public void testContains() { diff --git a/src/test/java/net/querz/nbt/LongArrayTagTest.java b/src/test/java/net/querz/nbt/tag/LongArrayTagTest.java similarity index 77% rename from src/test/java/net/querz/nbt/LongArrayTagTest.java rename to src/test/java/net/querz/nbt/tag/LongArrayTagTest.java index a2fe39c4..dfa2a3f3 100644 --- a/src/test/java/net/querz/nbt/LongArrayTagTest.java +++ b/src/test/java/net/querz/nbt/tag/LongArrayTagTest.java @@ -1,14 +1,28 @@ -package net.querz.nbt; +package net.querz.nbt.tag; + +import net.querz.NBTTestCase; import java.util.Arrays; public class LongArrayTagTest extends NBTTestCase { + public void testCreate() { + LongArrayTag t = new LongArrayTag(new long[]{Long.MIN_VALUE, 0, Long.MAX_VALUE}); + assertTrue(Arrays.equals(new long[]{Long.MIN_VALUE, 0, Long.MAX_VALUE}, t.getValue())); + t = new LongArrayTag(); + assertTrue(Arrays.equals(LongArrayTag.ZERO_VALUE, t.getValue())); + } + + public void testSetValue() { + LongArrayTag t = new LongArrayTag(); + t.setValue(new long[]{Long.MIN_VALUE, 0, Long.MAX_VALUE}); + assertTrue(Arrays.equals(new long[]{Long.MIN_VALUE, 0, Long.MAX_VALUE}, t.getValue())); + } + public void testStringConversion() { LongArrayTag t = new LongArrayTag(new long[]{Long.MIN_VALUE, 0, Long.MAX_VALUE}); assertTrue(Arrays.equals(new long[]{Long.MIN_VALUE, 0, Long.MAX_VALUE}, t.getValue())); assertEquals(12, t.getID()); - assertEquals("[L;-9223372036854775808l,0l,9223372036854775807l]", t.toTagString()); assertEquals("{\"type\":\"" + t.getClass().getSimpleName() + "\",\"value\":[-9223372036854775808,0,9223372036854775807]}", t.toString()); } diff --git a/src/test/java/net/querz/nbt/LongTagTest.java b/src/test/java/net/querz/nbt/tag/LongTagTest.java similarity index 78% rename from src/test/java/net/querz/nbt/LongTagTest.java rename to src/test/java/net/querz/nbt/tag/LongTagTest.java index 6d9ea1e1..86ef1ff2 100644 --- a/src/test/java/net/querz/nbt/LongTagTest.java +++ b/src/test/java/net/querz/nbt/tag/LongTagTest.java @@ -1,14 +1,28 @@ -package net.querz.nbt; +package net.querz.nbt.tag; + +import net.querz.NBTTestCase; import java.util.Arrays; public class LongTagTest extends NBTTestCase { + public void testCreate() { + LongTag t = new LongTag(Long.MAX_VALUE); + assertEquals(Long.MAX_VALUE, t.asLong()); + t = new LongTag(); + assertEquals(LongTag.ZERO_VALUE, t.asLong()); + } + + public void testSetValue() { + LongTag t = new LongTag(); + t.setValue(123); + assertEquals(123, t.asLong()); + } + public void testStringConversion() { LongTag t = new LongTag(Long.MAX_VALUE); assertEquals(Long.MAX_VALUE, t.asLong()); assertEquals(4, t.getID()); - assertEquals(Long.MAX_VALUE + "l", t.toTagString()); assertEquals("{\"type\":\"" + t.getClass().getSimpleName() + "\",\"value\":" + Long.MAX_VALUE + "}", t.toString()); } diff --git a/src/test/java/net/querz/nbt/NoNullEntrySetTest.java b/src/test/java/net/querz/nbt/tag/NoNullEntrySetTest.java similarity index 97% rename from src/test/java/net/querz/nbt/NoNullEntrySetTest.java rename to src/test/java/net/querz/nbt/tag/NoNullEntrySetTest.java index 6492300a..74d628b2 100644 --- a/src/test/java/net/querz/nbt/NoNullEntrySetTest.java +++ b/src/test/java/net/querz/nbt/tag/NoNullEntrySetTest.java @@ -1,4 +1,6 @@ -package net.querz.nbt; +package net.querz.nbt.tag; + +import net.querz.NBTTestCase; import java.util.Arrays; import java.util.Map; diff --git a/src/test/java/net/querz/nbt/ShortTagTest.java b/src/test/java/net/querz/nbt/tag/ShortTagTest.java similarity index 79% rename from src/test/java/net/querz/nbt/ShortTagTest.java rename to src/test/java/net/querz/nbt/tag/ShortTagTest.java index ec039a7d..9668c248 100644 --- a/src/test/java/net/querz/nbt/ShortTagTest.java +++ b/src/test/java/net/querz/nbt/tag/ShortTagTest.java @@ -1,16 +1,30 @@ -package net.querz.nbt; +package net.querz.nbt.tag; + +import net.querz.NBTTestCase; import java.util.Arrays; public class ShortTagTest extends NBTTestCase { + public void testCreate() { + ShortTag t = new ShortTag(Short.MAX_VALUE); + assertEquals(Short.MAX_VALUE, t.asShort()); + t = new ShortTag(); + assertEquals(ShortTag.ZERO_VALUE, t.asShort()); + } + + public void testSetValue() { + ShortTag t = new ShortTag(); + t.setValue((short) 123); + assertEquals(123, t.asShort()); + } + public void testStringConversion() { ShortTag t = new ShortTag(Short.MAX_VALUE); assertEquals(Short.MAX_VALUE, t.asShort()); assertEquals(Short.MAX_VALUE, t.asInt()); assertEquals(Short.MAX_VALUE, t.asLong()); assertEquals(2, t.getID()); - assertEquals(Short.MAX_VALUE + "s", t.toTagString()); assertEquals("{\"type\":\"" + t.getClass().getSimpleName() + "\",\"value\":" + Short.MAX_VALUE + "}", t.toString()); } diff --git a/src/test/java/net/querz/nbt/StringTagTest.java b/src/test/java/net/querz/nbt/tag/StringTagTest.java similarity index 82% rename from src/test/java/net/querz/nbt/StringTagTest.java rename to src/test/java/net/querz/nbt/tag/StringTagTest.java index da6b39fb..e008a55a 100644 --- a/src/test/java/net/querz/nbt/StringTagTest.java +++ b/src/test/java/net/querz/nbt/tag/StringTagTest.java @@ -1,4 +1,6 @@ -package net.querz.nbt; +package net.querz.nbt.tag; + +import net.querz.NBTTestCase; import java.util.Arrays; @@ -8,7 +10,6 @@ public void testStringConversion() { StringTag t = new StringTag("foo"); assertEquals("foo", t.getValue()); assertEquals(8, t.getID()); - assertEquals("foo", t.toTagString()); assertEquals("{\"type\":\"" + t.getClass().getSimpleName() + "\",\"value\":\"foo\"}", t.toString()); } @@ -37,13 +38,13 @@ public void testSerializeDeserialize() { public void testEscape() { StringTag allValue = new StringTag("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXZY0123456789_-+"); - assertEquals("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXZY0123456789_-+", allValue.toTagString()); + assertEquals("\"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXZY0123456789_-+\"", allValue.valueToString()); StringTag escapeValue = new StringTag("öäü"); - assertEquals("\"öäü\"", escapeValue.toTagString()); + assertEquals("\"öäü\"", escapeValue.valueToString()); StringTag escapeSpecialChars = new StringTag("\\\n\r\t\""); - assertEquals("\"\\\\\\n\\r\\t\\\"\"", escapeSpecialChars.toTagString()); + assertEquals("\"\\\\\\n\\r\\t\\\"\"", escapeSpecialChars.valueToString()); StringTag escapeEmpty = new StringTag(""); - assertEquals("\"\"", escapeEmpty.toTagString()); + assertEquals("\"\"", escapeEmpty.valueToString()); //no null values allowed assertThrowsRuntimeException(() -> new StringTag().setValue(null), NullPointerException.class); diff --git a/src/test/resources/r.0.0.mca b/src/test/resources/r.0.0.mca new file mode 100644 index 00000000..45146729 Binary files /dev/null and b/src/test/resources/r.0.0.mca differ diff --git a/src/test/resources/unknown_object_tag.dat b/src/test/resources/unknown_object_tag.dat deleted file mode 100644 index 2f2ed2c3..00000000 Binary files a/src/test/resources/unknown_object_tag.dat and /dev/null differ