├── settings.gradle ├── gradle.properties ├── samples ├── sample.nbt ├── samplegzip.nbt ├── samplezlib.nbt └── sample.json ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── main │ └── java │ │ └── dev │ │ └── dewy │ │ └── nbt │ │ ├── api │ │ ├── snbt │ │ │ ├── SnbtSerializable.java │ │ │ └── SnbtConfig.java │ │ ├── registry │ │ │ ├── TagTypeRegistryException.java │ │ │ └── TagTypeRegistry.java │ │ ├── json │ │ │ └── JsonSerializable.java │ │ └── Tag.java │ │ ├── io │ │ ├── CompressionType.java │ │ ├── NbtWriter.java │ │ └── NbtReader.java │ │ ├── utils │ │ └── StringUtils.java │ │ ├── tags │ │ ├── primitive │ │ │ ├── NumericalTag.java │ │ │ ├── IntTag.java │ │ │ ├── LongTag.java │ │ │ ├── FloatTag.java │ │ │ ├── DoubleTag.java │ │ │ ├── StringTag.java │ │ │ ├── ShortTag.java │ │ │ └── ByteTag.java │ │ ├── array │ │ │ ├── ArrayTag.java │ │ │ ├── ByteArrayTag.java │ │ │ ├── IntArrayTag.java │ │ │ └── LongArrayTag.java │ │ ├── TagType.java │ │ └── collection │ │ │ ├── ListTag.java │ │ │ └── CompoundTag.java │ │ └── Nbt.java └── test │ └── java │ └── dev │ └── dewy │ └── nbt │ └── test │ └── NbtTest.java ├── LICENSE.md ├── gradlew.bat ├── README.md ├── .gitignore ├── NBT.txt └── gradlew /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "nbt" 2 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | lang3=3.12.0 2 | gson=2.8.8 3 | lombok=1.18.20 4 | -------------------------------------------------------------------------------- /samples/sample.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitBuf/nbt/HEAD/samples/sample.nbt -------------------------------------------------------------------------------- /samples/samplegzip.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitBuf/nbt/HEAD/samples/samplegzip.nbt -------------------------------------------------------------------------------- /samples/samplezlib.nbt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitBuf/nbt/HEAD/samples/samplezlib.nbt -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitBuf/nbt/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /samples/sample.json: -------------------------------------------------------------------------------- 1 | {"type":10,"name":"root","value":{"primitive":{"type":3,"name":"primitive","value":3},"array":{"type":11,"name":"array","value":[0,1,2,3]},"list":{"type":9,"listType":8,"name":"list","value":[{"type":8,"value":"duck"},{"type":8,"value":"goose"}]},"compound":{"type":10,"name":"compound","value":{}}}} -------------------------------------------------------------------------------- /src/main/java/dev/dewy/nbt/api/snbt/SnbtSerializable.java: -------------------------------------------------------------------------------- 1 | package dev.dewy.nbt.api.snbt; 2 | 3 | import dev.dewy.nbt.api.registry.TagTypeRegistry; 4 | 5 | /** 6 | * Interface for SNBT serialization. Must be implemented if your tag will be SNBT serializable. Reading is not yet supported. 7 | * 8 | * @author dewy 9 | */ 10 | public interface SnbtSerializable { 11 | String toSnbt(int depth, TagTypeRegistry registry, SnbtConfig config); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/dev/dewy/nbt/api/snbt/SnbtConfig.java: -------------------------------------------------------------------------------- 1 | package dev.dewy.nbt.api.snbt; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * Configuration class for SNBT serialization. 7 | * 8 | * @author dewy 9 | */ 10 | @Data 11 | public class SnbtConfig { 12 | /** 13 | * Toggles SNBT pretty-printing. 14 | */ 15 | private boolean prettyPrint; 16 | 17 | /** 18 | * Defines the number of spaces " " to be used to indent in pretty-printing. 19 | */ 20 | private int indentSpaces = 4; 21 | 22 | /** 23 | * Defines the threshold at which array tags will be displayed inline rather than block to improve readability. 24 | */ 25 | private int inlineThreshold = 100; 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2021 iBuyMountainDew 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/main/java/dev/dewy/nbt/io/CompressionType.java: -------------------------------------------------------------------------------- 1 | package dev.dewy.nbt.io; 2 | 3 | import java.io.BufferedInputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | 7 | /** 8 | * Defines the types of compression supported by this library for NBT data. 9 | * 10 | * @author dewy 11 | */ 12 | public enum CompressionType { 13 | /** 14 | * No compression. 15 | */ 16 | NONE, 17 | 18 | /** 19 | * GZIP compression ({@code GZIPInputStream} and {@code GZIPOutputStream}). 20 | */ 21 | GZIP, 22 | 23 | /** 24 | * ZLIB compression ({@code InflaterInputStream} and {@code DeflaterOutputStream}). 25 | */ 26 | ZLIB; 27 | 28 | public static CompressionType getCompression(InputStream in) throws IOException { 29 | if (!in.markSupported()) { 30 | in = new BufferedInputStream(in); 31 | } 32 | in.mark(0); 33 | 34 | if (in.read() == 120) { 35 | return ZLIB; 36 | } 37 | 38 | in.reset(); 39 | if (in.read() == 31) { 40 | return GZIP; 41 | } 42 | 43 | return NONE; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/dev/dewy/nbt/api/registry/TagTypeRegistryException.java: -------------------------------------------------------------------------------- 1 | package dev.dewy.nbt.api.registry; 2 | 3 | /** 4 | * Checked exception thrown when any issue arises relating to the {@link TagTypeRegistry}. 5 | * 6 | * @author dewy 7 | */ 8 | public class TagTypeRegistryException extends Exception { 9 | /** 10 | * Constructs a new {@link TagTypeRegistryException} with the specified detail message. The cause is not initialized, and may subsequently be initialized by a call to initCause. 11 | * 12 | * @param message the detail message. The detail message is saved for later retrieval by the getMessage() method. 13 | */ 14 | public TagTypeRegistryException(String message) { 15 | super(message); 16 | } 17 | 18 | /** 19 | * Constructs a new {@link TagTypeRegistryException} with the specified detail message and cause. 20 | * Note that the detail message associated with cause is not automatically incorporated in this exception's detail message. 21 | * 22 | * @param message the detail message (which is saved for later retrieval by the getMessage() method). 23 | * @param cause the cause (which is saved for later retrieval by the getCause() method). (A null value is permitted, and indicates that the cause is nonexistent or unknown.) 24 | */ 25 | public TagTypeRegistryException(String message, Throwable cause) { 26 | super(message, cause); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/dev/dewy/nbt/api/json/JsonSerializable.java: -------------------------------------------------------------------------------- 1 | package dev.dewy.nbt.api.json; 2 | 3 | import com.google.gson.JsonObject; 4 | import dev.dewy.nbt.api.Tag; 5 | import dev.dewy.nbt.api.registry.TagTypeRegistry; 6 | 7 | import java.io.IOException; 8 | 9 | /** 10 | * Interface for JSON (de)serialization. Must be implemented if your tag will be JSON (de)serializable. 11 | * 12 | * @author dewy 13 | */ 14 | public interface JsonSerializable { 15 | /** 16 | * Serializes this tag into a GSON {@code JsonObject}. 17 | * 18 | * @param depth the current depth of the NBT data structure. 19 | * @param registry the {@link TagTypeRegistry} to be used in serialization. 20 | * @return the serialized {@code JsonObject}. 21 | * @throws IOException if any I/O error occurs. 22 | */ 23 | JsonObject toJson(int depth, TagTypeRegistry registry) throws IOException; 24 | 25 | /** 26 | * Deserializes this tag from a give {@code JsonObject}. 27 | * 28 | * @param json the {@code JsonObject} to be deserialized. 29 | * @param depth the current depth of the NBT data structure. 30 | * @param registry the {@link TagTypeRegistry} to be used in deserialization. 31 | * @return this (literally {@code return this;} after deserialization). 32 | * @throws IOException if any I/O error occurs. 33 | */ 34 | Tag fromJson(JsonObject json, int depth, TagTypeRegistry registry) throws IOException; 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/dev/dewy/nbt/utils/StringUtils.java: -------------------------------------------------------------------------------- 1 | package dev.dewy.nbt.utils; 2 | 3 | import dev.dewy.nbt.api.snbt.SnbtConfig; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.regex.Matcher; 8 | import java.util.regex.Pattern; 9 | 10 | public class StringUtils { 11 | public static String escapeSnbt(String str) { 12 | StringBuilder sb = new StringBuilder(" "); 13 | 14 | char quote = 0; 15 | for (int i = 0; i < str.length(); ++i) { 16 | char current = str.charAt(i); 17 | 18 | if (current == '\\') { 19 | sb.append('\\'); 20 | } else if (current == '"' || current == '\'') { 21 | if (quote == 0) { 22 | quote = current == '"' ? '\'' : '"'; 23 | } 24 | 25 | if (quote == current) { 26 | sb.append('\\'); 27 | } 28 | } 29 | 30 | sb.append(current); 31 | } 32 | 33 | if (quote == 0) { 34 | quote = '"'; 35 | } 36 | 37 | sb.setCharAt(0, quote); 38 | sb.append(quote); 39 | 40 | return sb.toString(); 41 | } 42 | 43 | public static String multiplyIndent(int by, SnbtConfig config) { 44 | return new String(new char[by * config.getIndentSpaces()]).replace("\0", " "); 45 | } 46 | 47 | public static String[] getMatches(Pattern regex, String in) { 48 | List matches = new ArrayList<>(); 49 | Matcher matcher = regex.matcher(in); 50 | 51 | while (matcher.find()) { 52 | matches.add(matcher.group()); 53 | } 54 | 55 | return matches.toArray(new String[0]); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/dev/dewy/nbt/io/NbtWriter.java: -------------------------------------------------------------------------------- 1 | package dev.dewy.nbt.io; 2 | 3 | import dev.dewy.nbt.api.registry.TagTypeRegistry; 4 | import dev.dewy.nbt.tags.TagType; 5 | import dev.dewy.nbt.tags.collection.CompoundTag; 6 | import lombok.AllArgsConstructor; 7 | import lombok.NonNull; 8 | 9 | import java.io.DataOutput; 10 | import java.io.IOException; 11 | 12 | /** 13 | * Used to write root {@link CompoundTag}s using a certain {@link TagTypeRegistry}. 14 | * 15 | * @author dewy 16 | */ 17 | @AllArgsConstructor 18 | public class NbtWriter { 19 | private @NonNull TagTypeRegistry typeRegistry; 20 | 21 | /** 22 | * Writes the given root {@link CompoundTag} to a {@link DataOutput} stream. 23 | * 24 | * @param compound the NBT structure to write, contained within a {@link CompoundTag}. 25 | * @param output the stream to write to. 26 | * @throws IOException if any I/O error occurs. 27 | */ 28 | public void toStream(@NonNull CompoundTag compound, @NonNull DataOutput output) throws IOException { 29 | output.writeByte(TagType.COMPOUND.getId()); 30 | 31 | if (compound.getName() == null) { 32 | output.writeUTF(""); 33 | } else { 34 | output.writeUTF(compound.getName()); 35 | } 36 | 37 | compound.write(output, 0, this.typeRegistry); 38 | } 39 | 40 | /** 41 | * Returns the {@link TagTypeRegistry} currently in use by this writer. 42 | * 43 | * @return the {@link TagTypeRegistry} currently in use by this writer. 44 | */ 45 | public TagTypeRegistry getTypeRegistry() { 46 | return typeRegistry; 47 | } 48 | 49 | /** 50 | * Sets the {@link TagTypeRegistry} currently in use by this writer. Used to utilise custom-made tag types. 51 | * 52 | * @param typeRegistry the new {@link TagTypeRegistry} to be set. 53 | */ 54 | public void setTypeRegistry(@NonNull TagTypeRegistry typeRegistry) { 55 | this.typeRegistry = typeRegistry; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/dev/dewy/nbt/io/NbtReader.java: -------------------------------------------------------------------------------- 1 | package dev.dewy.nbt.io; 2 | 3 | import dev.dewy.nbt.api.registry.TagTypeRegistry; 4 | import dev.dewy.nbt.tags.TagType; 5 | import dev.dewy.nbt.tags.collection.CompoundTag; 6 | import lombok.AllArgsConstructor; 7 | import lombok.NonNull; 8 | 9 | import java.io.DataInput; 10 | import java.io.IOException; 11 | 12 | /** 13 | * Used to read root {@link CompoundTag}s using a certain {@link TagTypeRegistry}. 14 | * 15 | * @author dewy 16 | */ 17 | @AllArgsConstructor 18 | public class NbtReader { 19 | private @NonNull TagTypeRegistry typeRegistry; 20 | 21 | /** 22 | * Reads a root {@link CompoundTag} from a {@link DataInput} stream. 23 | * 24 | * @param input the stream to read from. 25 | * @return the root {@link CompoundTag} read from the stream. 26 | * @throws IOException if any I/O error occurs. 27 | */ 28 | public CompoundTag fromStream(@NonNull DataInput input) throws IOException { 29 | if (input.readByte() != TagType.COMPOUND.getId()) { 30 | throw new IOException("Root tag in NBT structure must be a compound tag."); 31 | } 32 | 33 | CompoundTag result = new CompoundTag(); 34 | 35 | result.setName(input.readUTF()); 36 | result.read(input, 0, this.typeRegistry); 37 | 38 | return result; 39 | } 40 | 41 | /** 42 | * Returns the {@link TagTypeRegistry} currently in use by this reader. 43 | * 44 | * @return the {@link TagTypeRegistry} currently in use by this reader. 45 | */ 46 | public TagTypeRegistry getTypeRegistry() { 47 | return typeRegistry; 48 | } 49 | 50 | /** 51 | * Sets the {@link TagTypeRegistry} currently in use by this reader. Used to utilise custom-made tag types. 52 | * 53 | * @param typeRegistry the new {@link TagTypeRegistry} to be set. 54 | */ 55 | public void setTypeRegistry(@NonNull TagTypeRegistry typeRegistry) { 56 | this.typeRegistry = typeRegistry; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/dev/dewy/nbt/api/Tag.java: -------------------------------------------------------------------------------- 1 | package dev.dewy.nbt.api; 2 | 3 | import dev.dewy.nbt.api.registry.TagTypeRegistry; 4 | 5 | import java.io.DataInput; 6 | import java.io.DataOutput; 7 | import java.io.IOException; 8 | 9 | /** 10 | * An abstract NBT tag. 11 | * 12 | * @author dewy 13 | */ 14 | public abstract class Tag { 15 | private String name; 16 | 17 | /** 18 | * Returns the name (key) of this tag. 19 | * 20 | * @return the name (key) of this tag. 21 | */ 22 | public String getName() { 23 | return name; 24 | } 25 | 26 | /** 27 | * Sets the name (key) of this tag. 28 | * 29 | * @param name the new name to be set. 30 | */ 31 | public void setName(String name) { 32 | this.name = name; 33 | } 34 | 35 | /** 36 | * Returns a unique ID for this NBT tag type. 0 to 12 (inclusive) are reserved. 37 | * 38 | * @return a unique ID for this NBT tag type. 39 | */ 40 | public abstract byte getTypeId(); 41 | 42 | /** 43 | * Returns the value held by this tag. 44 | * 45 | * @return the value held by this tag. 46 | */ 47 | public abstract Object getValue(); 48 | 49 | /** 50 | * Writes this tag to a {@link DataOutput} stream. 51 | * 52 | * @param output the stream to write to. 53 | * @param depth the current depth of the NBT data structure. 54 | * @param registry the {@link TagTypeRegistry} to be used in writing. 55 | * @throws IOException if any I/O error occurs. 56 | */ 57 | public abstract void write(DataOutput output, int depth, TagTypeRegistry registry) throws IOException; 58 | 59 | /** 60 | * Reads this tag from a {@link DataInput} stream. 61 | * 62 | * @param input the stream to read from. 63 | * @param depth the current depth of the NBT data structure. 64 | * @param registry the {@link TagTypeRegistry} to be used in reading. 65 | * @return this (literally {@code return this;} after reading). 66 | * @throws IOException if any I/O error occurs. 67 | */ 68 | public abstract Tag read(DataInput input, int depth, TagTypeRegistry registry) throws IOException; 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/dev/dewy/nbt/tags/primitive/NumericalTag.java: -------------------------------------------------------------------------------- 1 | package dev.dewy.nbt.tags.primitive; 2 | 3 | import dev.dewy.nbt.api.Tag; 4 | import dev.dewy.nbt.api.json.JsonSerializable; 5 | import dev.dewy.nbt.api.registry.TagTypeRegistry; 6 | import dev.dewy.nbt.api.snbt.SnbtConfig; 7 | import dev.dewy.nbt.api.snbt.SnbtSerializable; 8 | 9 | /** 10 | * An abstract superclass of all NBT tags representing numeric values that can be converted to the primitive types. 11 | * 12 | * @param the {@code Number} type this NBT tag represents. 13 | * @author dewy 14 | */ 15 | public abstract class NumericalTag extends Tag implements SnbtSerializable, JsonSerializable { 16 | @Override 17 | public abstract T getValue(); 18 | 19 | /** 20 | * Returns the value held by this tag as a primitive {@code byte}. 21 | * 22 | * @return the value held by this tag as a primitive {@code byte}. 23 | */ 24 | public byte byteValue() { 25 | return this.getValue().byteValue(); 26 | } 27 | 28 | /** 29 | * Returns the value held by this tag as a primitive {@code short}. 30 | * 31 | * @return the value held by this tag as a primitive {@code short}. 32 | */ 33 | public short shortValue() { 34 | return this.getValue().shortValue(); 35 | } 36 | 37 | /** 38 | * Returns the value held by this tag as a primitive {@code int}. 39 | * 40 | * @return the value held by this tag as a primitive {@code int}. 41 | */ 42 | public int intValue() { 43 | return this.getValue().intValue(); 44 | } 45 | 46 | /** 47 | * Returns the value held by this tag as a primitive {@code long}. 48 | * 49 | * @return the value held by this tag as a primitive {@code long}. 50 | */ 51 | public long longValue() { 52 | return this.getValue().longValue(); 53 | } 54 | 55 | /** 56 | * Returns the value held by this tag as a primitive {@code float}. 57 | * 58 | * @return the value held by this tag as a primitive {@code float}. 59 | */ 60 | public float floatValue() { 61 | return this.getValue().floatValue(); 62 | } 63 | 64 | /** 65 | * Returns the value held by this tag as a primitive {@code double}. 66 | * 67 | * @return the value held by this tag as a primitive {@code double}. 68 | */ 69 | public double doubleValue() { 70 | return this.getValue().doubleValue(); 71 | } 72 | 73 | @Override 74 | public String toString() { 75 | return this.toSnbt(0, new TagTypeRegistry(), new SnbtConfig()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/dev/dewy/nbt/tags/array/ArrayTag.java: -------------------------------------------------------------------------------- 1 | package dev.dewy.nbt.tags.array; 2 | 3 | import dev.dewy.nbt.api.Tag; 4 | import dev.dewy.nbt.api.json.JsonSerializable; 5 | import dev.dewy.nbt.api.registry.TagTypeRegistry; 6 | import dev.dewy.nbt.api.snbt.SnbtConfig; 7 | import dev.dewy.nbt.api.snbt.SnbtSerializable; 8 | 9 | import java.util.regex.Pattern; 10 | 11 | /** 12 | * Abstract class for implementing NBT array tags. 13 | * 14 | * @param the type held in the array. 15 | * @author dewy 16 | */ 17 | public abstract class ArrayTag extends Tag implements SnbtSerializable, JsonSerializable, Iterable { 18 | public static final Pattern NUMBER_PATTERN = Pattern.compile("[-0-9]+"); 19 | 20 | /** 21 | * Returns the number of elements in this array tag. 22 | * 23 | * @return the number of elements in this array tag. 24 | */ 25 | public abstract int size(); 26 | 27 | /** 28 | * Returns the element at the specified position in this array tag. 29 | * 30 | * @param index index of the element to return. 31 | * @return the element at the specified position in this array tag. 32 | */ 33 | public abstract T get(int index); 34 | 35 | /** 36 | * Replaces the element at the specified position in this array tag with the specified element. 37 | * 38 | * @param index index of the element to replace. 39 | * @param element element to be stored at the specified position. 40 | * @return the element previously at the specified position. 41 | */ 42 | public abstract T set(int index, T element); 43 | 44 | /** 45 | * Inserts the specified element(s) at the specified position in this array tag. 46 | * Shifts the element(s) currently at that position and any subsequent elements to the right. 47 | * 48 | * @param index index at which the element(s) are to be inserted. 49 | * @param elements element(s) to be inserted. 50 | */ 51 | public abstract void insert(int index, T... elements); 52 | 53 | /** 54 | * Appends the specified element(s) to the end of the array tag. 55 | * 56 | * @param elements element(s) to be added. 57 | */ 58 | @SafeVarargs 59 | public final void add(T... elements) { 60 | this.insert(this.size() - 1, elements); 61 | } 62 | 63 | /** 64 | * Removes the element at the specified position in this array tag. 65 | * Shifts any subsequent elements to the left. Returns the element that was removed from the array tag. 66 | * 67 | * @param index the index of the element to be removed. 68 | * @return the element previously at the specified position. 69 | */ 70 | public abstract T remove(int index); 71 | 72 | /** 73 | * Removes all the elements from this array tag. The array tag will be empty after this call returns. 74 | */ 75 | public abstract void clear(); 76 | 77 | @Override 78 | public String toString() { 79 | return this.toSnbt(0, new TagTypeRegistry(), new SnbtConfig()); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /src/main/java/dev/dewy/nbt/tags/primitive/IntTag.java: -------------------------------------------------------------------------------- 1 | package dev.dewy.nbt.tags.primitive; 2 | 3 | import com.google.gson.JsonObject; 4 | import dev.dewy.nbt.api.registry.TagTypeRegistry; 5 | import dev.dewy.nbt.api.snbt.SnbtConfig; 6 | import dev.dewy.nbt.tags.TagType; 7 | import lombok.AllArgsConstructor; 8 | import lombok.NoArgsConstructor; 9 | 10 | import java.io.DataInput; 11 | import java.io.DataOutput; 12 | import java.io.IOException; 13 | 14 | /** 15 | * The int tag (type ID 3) is used for storing a 32-bit signed two's complement integer; a Java primitive {@code int}. 16 | * 17 | * @author dewy 18 | */ 19 | @NoArgsConstructor 20 | @AllArgsConstructor 21 | public class IntTag extends NumericalTag { 22 | private int value; 23 | 24 | /** 25 | * Constructs an int tag with a given name and value. 26 | * 27 | * @param name the tag's name. 28 | * @param value the tag's {@code int} value. 29 | */ 30 | public IntTag(String name, int value) { 31 | this.setName(name); 32 | this.setValue(value); 33 | } 34 | 35 | @Override 36 | public byte getTypeId() { 37 | return TagType.INT.getId(); 38 | } 39 | 40 | @Override 41 | public Integer getValue() { 42 | return this.value; 43 | } 44 | 45 | /** 46 | * Sets the {@code int} value of this int tag. 47 | * 48 | * @param value new {@code int} value to be set. 49 | */ 50 | public void setValue(int value) { 51 | this.value = value; 52 | } 53 | 54 | @Override 55 | public void write(DataOutput output, int depth, TagTypeRegistry registry) throws IOException { 56 | output.writeInt(this.value); 57 | } 58 | 59 | @Override 60 | public IntTag read(DataInput input, int depth, TagTypeRegistry registry) throws IOException { 61 | this.value = input.readInt(); 62 | 63 | return this; 64 | } 65 | 66 | @Override 67 | public String toSnbt(int depth, TagTypeRegistry registry, SnbtConfig config) { 68 | return Integer.toString(this.value); 69 | } 70 | 71 | @Override 72 | public JsonObject toJson(int depth, TagTypeRegistry registry) { 73 | JsonObject json = new JsonObject(); 74 | json.addProperty("type", this.getTypeId()); 75 | 76 | if (this.getName() != null) { 77 | json.addProperty("name", this.getName()); 78 | } 79 | 80 | json.addProperty("value", this.value); 81 | 82 | return json; 83 | } 84 | 85 | @Override 86 | public IntTag fromJson(JsonObject json, int depth, TagTypeRegistry registry) { 87 | if (json.has("name")) { 88 | this.setName(json.getAsJsonPrimitive("name").getAsString()); 89 | } else { 90 | this.setName(null); 91 | } 92 | 93 | this.value = json.getAsJsonPrimitive("value").getAsInt(); 94 | 95 | return this; 96 | } 97 | 98 | @Override 99 | public boolean equals(Object o) { 100 | if (this == o) return true; 101 | if (o == null || getClass() != o.getClass()) return false; 102 | 103 | IntTag intTag = (IntTag) o; 104 | 105 | return value == intTag.value; 106 | } 107 | 108 | @Override 109 | public int hashCode() { 110 | return value; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/dev/dewy/nbt/tags/TagType.java: -------------------------------------------------------------------------------- 1 | package dev.dewy.nbt.tags; 2 | 3 | import dev.dewy.nbt.api.registry.TagTypeRegistry; 4 | import dev.dewy.nbt.api.registry.TagTypeRegistryException; 5 | import dev.dewy.nbt.tags.array.ByteArrayTag; 6 | import dev.dewy.nbt.tags.array.IntArrayTag; 7 | import dev.dewy.nbt.tags.array.LongArrayTag; 8 | import dev.dewy.nbt.tags.collection.CompoundTag; 9 | import dev.dewy.nbt.tags.collection.ListTag; 10 | import dev.dewy.nbt.tags.primitive.*; 11 | 12 | /** 13 | * Defines the 12 standard NBT tag types and their IDs supported by this library, laid out in the Notchian spec. 14 | * 15 | * @author dewy 16 | */ 17 | public enum TagType { 18 | /** 19 | * ID: 1 20 | * 21 | * @see ByteTag 22 | */ 23 | BYTE(1), 24 | 25 | /** 26 | * ID: 2 27 | * 28 | * @see ShortTag 29 | */ 30 | SHORT(2), 31 | 32 | /** 33 | * ID: 3 34 | * 35 | * @see IntTag 36 | */ 37 | INT(3), 38 | 39 | /** 40 | * ID: 4 41 | * 42 | * @see LongTag 43 | */ 44 | LONG(4), 45 | 46 | /** 47 | * ID: 5 48 | * 49 | * @see FloatTag 50 | */ 51 | FLOAT(5), 52 | 53 | /** 54 | * ID: 6 55 | * 56 | * @see DoubleTag 57 | */ 58 | DOUBLE(6), 59 | 60 | /** 61 | * ID: 7 62 | * 63 | * @see ByteArrayTag 64 | */ 65 | BYTE_ARRAY(7), 66 | 67 | /** 68 | * ID: 8 69 | * 70 | * @see StringTag 71 | */ 72 | STRING(8), 73 | 74 | /** 75 | * ID: 9 76 | * 77 | * @see ListTag 78 | */ 79 | LIST(9), 80 | 81 | /** 82 | * ID: 10 83 | * 84 | * @see CompoundTag 85 | */ 86 | COMPOUND(10), 87 | 88 | /** 89 | * ID: 11 90 | * 91 | * @see IntArrayTag 92 | */ 93 | INT_ARRAY(11), 94 | 95 | /** 96 | * ID: 12 97 | * 98 | * @see LongArrayTag 99 | */ 100 | LONG_ARRAY(12); 101 | 102 | private final int id; 103 | 104 | TagType(int id) { 105 | this.id = id; 106 | } 107 | 108 | public byte getId() { 109 | return (byte) id; 110 | } 111 | 112 | public static void registerAll(TagTypeRegistry registry) { 113 | try { 114 | registry.registerTagType(BYTE.getId(), ByteTag.class); 115 | registry.registerTagType(SHORT.getId(), ShortTag.class); 116 | registry.registerTagType(INT.getId(), IntTag.class); 117 | registry.registerTagType(LONG.getId(), LongTag.class); 118 | registry.registerTagType(FLOAT.getId(), FloatTag.class); 119 | registry.registerTagType(DOUBLE.getId(), DoubleTag.class); 120 | registry.registerTagType(BYTE_ARRAY.getId(), ByteArrayTag.class); 121 | registry.registerTagType(STRING.getId(), StringTag.class); 122 | registry.registerTagType(LIST.getId(), ListTag.class); 123 | registry.registerTagType(COMPOUND.getId(), CompoundTag.class); 124 | registry.registerTagType(INT_ARRAY.getId(), IntArrayTag.class); 125 | registry.registerTagType(LONG_ARRAY.getId(), LongArrayTag.class); 126 | } catch (TagTypeRegistryException e) { 127 | // Should never happen. 128 | e.printStackTrace(); 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/dev/dewy/nbt/tags/primitive/LongTag.java: -------------------------------------------------------------------------------- 1 | package dev.dewy.nbt.tags.primitive; 2 | 3 | import com.google.gson.JsonObject; 4 | import dev.dewy.nbt.api.registry.TagTypeRegistry; 5 | import dev.dewy.nbt.api.snbt.SnbtConfig; 6 | import dev.dewy.nbt.tags.TagType; 7 | import lombok.AllArgsConstructor; 8 | import lombok.NoArgsConstructor; 9 | 10 | import java.io.DataInput; 11 | import java.io.DataOutput; 12 | import java.io.IOException; 13 | 14 | /** 15 | * The long tag (type ID 4) is used for storing a 64-bit signed two's complement integer; a Java primitive {@code long}. 16 | * 17 | * @author dewy 18 | */ 19 | @NoArgsConstructor 20 | @AllArgsConstructor 21 | public class LongTag extends NumericalTag { 22 | private long value; 23 | 24 | /** 25 | * Constructs a long tag with a given name and value. 26 | * 27 | * @param name the tag's name. 28 | * @param value the tag's {@code long} value. 29 | */ 30 | public LongTag(String name, long value) { 31 | this.setName(name); 32 | this.setValue(value); 33 | } 34 | 35 | @Override 36 | public byte getTypeId() { 37 | return TagType.LONG.getId(); 38 | } 39 | 40 | @Override 41 | public Long getValue() { 42 | return this.value; 43 | } 44 | 45 | /** 46 | * Sets the {@code long} value of this long tag. 47 | * 48 | * @param value new {@code long} value to be set. 49 | */ 50 | public void setValue(long value) { 51 | this.value = value; 52 | } 53 | 54 | @Override 55 | public void write(DataOutput output, int depth, TagTypeRegistry registry) throws IOException { 56 | output.writeLong(this.value); 57 | } 58 | 59 | @Override 60 | public LongTag read(DataInput input, int depth, TagTypeRegistry registry) throws IOException { 61 | this.value = input.readLong(); 62 | 63 | return this; 64 | } 65 | 66 | @Override 67 | public String toSnbt(int depth, TagTypeRegistry registry, SnbtConfig config) { 68 | return this.value + "L"; 69 | } 70 | 71 | @Override 72 | public JsonObject toJson(int depth, TagTypeRegistry registry) { 73 | JsonObject json = new JsonObject(); 74 | json.addProperty("type", this.getTypeId()); 75 | 76 | if (this.getName() != null) { 77 | json.addProperty("name", this.getName()); 78 | } 79 | 80 | json.addProperty("value", this.value); 81 | 82 | return json; 83 | } 84 | 85 | @Override 86 | public LongTag fromJson(JsonObject json, int depth, TagTypeRegistry registry) { 87 | if (json.has("name")) { 88 | this.setName(json.getAsJsonPrimitive("name").getAsString()); 89 | } else { 90 | this.setName(null); 91 | } 92 | 93 | this.value = json.getAsJsonPrimitive("value").getAsLong(); 94 | 95 | return this; 96 | } 97 | 98 | @Override 99 | public boolean equals(Object o) { 100 | if (this == o) return true; 101 | if (o == null || getClass() != o.getClass()) return false; 102 | 103 | LongTag longTag = (LongTag) o; 104 | 105 | return value == longTag.value; 106 | } 107 | 108 | @Override 109 | public int hashCode() { 110 | return (int) (value ^ (value >>> 32)); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/dev/dewy/nbt/tags/primitive/FloatTag.java: -------------------------------------------------------------------------------- 1 | package dev.dewy.nbt.tags.primitive; 2 | 3 | import com.google.gson.JsonObject; 4 | import dev.dewy.nbt.api.registry.TagTypeRegistry; 5 | import dev.dewy.nbt.api.snbt.SnbtConfig; 6 | import dev.dewy.nbt.tags.TagType; 7 | import lombok.AllArgsConstructor; 8 | import lombok.NoArgsConstructor; 9 | 10 | import java.io.DataInput; 11 | import java.io.DataOutput; 12 | import java.io.IOException; 13 | 14 | /** 15 | * The float tag (type ID 5) is used for storing a single-precision 32-bit IEEE 754 floating point value; a Java primitive {@code float}. 16 | * 17 | * @author dewy 18 | */ 19 | @NoArgsConstructor 20 | @AllArgsConstructor 21 | public class FloatTag extends NumericalTag { 22 | private float value; 23 | 24 | /** 25 | * Constructs a float tag with a given name and value. 26 | * 27 | * @param name the tag's name. 28 | * @param value the tag's {@code float} value. 29 | */ 30 | public FloatTag(String name, float value) { 31 | this.setName(name); 32 | this.setValue(value); 33 | } 34 | 35 | @Override 36 | public byte getTypeId() { 37 | return TagType.FLOAT.getId(); 38 | } 39 | 40 | @Override 41 | public Float getValue() { 42 | return this.value; 43 | } 44 | 45 | /** 46 | * Sets the {@code float} value of this float tag. 47 | * 48 | * @param value new {@code float} value to be set. 49 | */ 50 | public void setValue(float value) { 51 | this.value = value; 52 | } 53 | 54 | @Override 55 | public void write(DataOutput output, int depth, TagTypeRegistry registry) throws IOException { 56 | output.writeFloat(this.value); 57 | } 58 | 59 | @Override 60 | public FloatTag read(DataInput input, int depth, TagTypeRegistry registry) throws IOException { 61 | this.value = input.readFloat(); 62 | 63 | return this; 64 | } 65 | 66 | @Override 67 | public String toSnbt(int depth, TagTypeRegistry registry, SnbtConfig config) { 68 | return this.value + "f"; 69 | } 70 | 71 | @Override 72 | public JsonObject toJson(int depth, TagTypeRegistry registry) { 73 | JsonObject json = new JsonObject(); 74 | json.addProperty("type", this.getTypeId()); 75 | 76 | if (this.getName() != null) { 77 | json.addProperty("name", this.getName()); 78 | } 79 | 80 | json.addProperty("value", this.value); 81 | 82 | return json; 83 | } 84 | 85 | @Override 86 | public FloatTag fromJson(JsonObject json, int depth, TagTypeRegistry registry) { 87 | if (json.has("name")) { 88 | this.setName(json.getAsJsonPrimitive("name").getAsString()); 89 | } else { 90 | this.setName(null); 91 | } 92 | 93 | this.value = json.getAsJsonPrimitive("value").getAsFloat(); 94 | 95 | return this; 96 | } 97 | 98 | @Override 99 | public boolean equals(Object o) { 100 | if (this == o) return true; 101 | if (o == null || getClass() != o.getClass()) return false; 102 | 103 | FloatTag floatTag = (FloatTag) o; 104 | 105 | return Float.compare(floatTag.value, value) == 0; 106 | } 107 | 108 | @Override 109 | public int hashCode() { 110 | return (value != 0.0f ? Float.floatToIntBits(value) : 0); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/dev/dewy/nbt/tags/primitive/DoubleTag.java: -------------------------------------------------------------------------------- 1 | package dev.dewy.nbt.tags.primitive; 2 | 3 | import com.google.gson.JsonObject; 4 | import dev.dewy.nbt.api.registry.TagTypeRegistry; 5 | import dev.dewy.nbt.api.snbt.SnbtConfig; 6 | import dev.dewy.nbt.tags.TagType; 7 | import lombok.AllArgsConstructor; 8 | import lombok.NoArgsConstructor; 9 | 10 | import java.io.DataInput; 11 | import java.io.DataOutput; 12 | import java.io.IOException; 13 | 14 | /** 15 | * The double tag (type ID 6) is used for storing a double-precision 64-bit IEEE 754 floating point value; a Java primitive {@code double}. 16 | * 17 | * @author dewy 18 | */ 19 | @NoArgsConstructor 20 | @AllArgsConstructor 21 | public class DoubleTag extends NumericalTag { 22 | private double value; 23 | 24 | /** 25 | * Constructs a double tag with a given name and value. 26 | * 27 | * @param name the tag's name. 28 | * @param value the tag's {@code double} value. 29 | */ 30 | public DoubleTag(String name, double value) { 31 | this.setName(name); 32 | this.setValue(value); 33 | } 34 | 35 | @Override 36 | public byte getTypeId() { 37 | return TagType.DOUBLE.getId(); 38 | } 39 | 40 | @Override 41 | public Double getValue() { 42 | return this.value; 43 | } 44 | 45 | /** 46 | * Sets the {@code double} value of this double tag. 47 | * 48 | * @param value new {@code double} value to be set. 49 | */ 50 | public void setValue(double value) { 51 | this.value = value; 52 | } 53 | 54 | @Override 55 | public void write(DataOutput output, int depth, TagTypeRegistry registry) throws IOException { 56 | output.writeDouble(this.value); 57 | } 58 | 59 | @Override 60 | public DoubleTag read(DataInput input, int depth, TagTypeRegistry registry) throws IOException { 61 | this.value = input.readDouble(); 62 | 63 | return this; 64 | } 65 | 66 | @Override 67 | public String toSnbt(int depth, TagTypeRegistry registry, SnbtConfig config) { 68 | return this.value + "d"; 69 | } 70 | 71 | @Override 72 | public JsonObject toJson(int depth, TagTypeRegistry registry) { 73 | JsonObject json = new JsonObject(); 74 | json.addProperty("type", this.getTypeId()); 75 | 76 | if (this.getName() != null) { 77 | json.addProperty("name", this.getName()); 78 | } 79 | 80 | json.addProperty("value", this.value); 81 | 82 | return json; 83 | } 84 | 85 | @Override 86 | public DoubleTag fromJson(JsonObject json, int depth, TagTypeRegistry registry) { 87 | if (json.has("name")) { 88 | this.setName(json.getAsJsonPrimitive("name").getAsString()); 89 | } else { 90 | this.setName(null); 91 | } 92 | 93 | this.value = json.getAsJsonPrimitive("value").getAsDouble(); 94 | 95 | return this; 96 | } 97 | 98 | @Override 99 | public boolean equals(Object o) { 100 | if (this == o) return true; 101 | if (o == null || getClass() != o.getClass()) return false; 102 | 103 | DoubleTag doubleTag = (DoubleTag) o; 104 | 105 | return Double.compare(doubleTag.value, value) == 0; 106 | } 107 | 108 | @Override 109 | public int hashCode() { 110 | long temp = Double.doubleToLongBits(value); 111 | return (int) (temp ^ (temp >>> 32)); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/dev/dewy/nbt/tags/primitive/StringTag.java: -------------------------------------------------------------------------------- 1 | package dev.dewy.nbt.tags.primitive; 2 | 3 | import com.google.gson.JsonObject; 4 | import dev.dewy.nbt.api.Tag; 5 | import dev.dewy.nbt.api.json.JsonSerializable; 6 | import dev.dewy.nbt.api.registry.TagTypeRegistry; 7 | import dev.dewy.nbt.api.snbt.SnbtConfig; 8 | import dev.dewy.nbt.api.snbt.SnbtSerializable; 9 | import dev.dewy.nbt.tags.TagType; 10 | import dev.dewy.nbt.utils.StringUtils; 11 | import lombok.AllArgsConstructor; 12 | import lombok.NoArgsConstructor; 13 | import lombok.NonNull; 14 | 15 | import java.io.DataInput; 16 | import java.io.DataOutput; 17 | import java.io.IOException; 18 | import java.util.Objects; 19 | 20 | /** 21 | * The string tag (type ID 8) is used for storing a UTF-8 encoded {@code String}, prefixed by a length value stored as a 32-bit {@code int}. 22 | * 23 | * @author dewy 24 | */ 25 | @NoArgsConstructor 26 | @AllArgsConstructor 27 | public class StringTag extends Tag implements SnbtSerializable, JsonSerializable { 28 | private @NonNull String value; 29 | 30 | /** 31 | * Constructs a string tag with a given name and value. 32 | * 33 | * @param name the tag's name. 34 | * @param value the tag's {@code String} value. 35 | */ 36 | public StringTag(String name, @NonNull String value) { 37 | this.setName(name); 38 | this.setValue(value); 39 | } 40 | 41 | @Override 42 | public byte getTypeId() { 43 | return TagType.STRING.getId(); 44 | } 45 | 46 | @Override 47 | public String getValue() { 48 | return this.value; 49 | } 50 | 51 | /** 52 | * Sets the {@code String} value of this string tag. 53 | * 54 | * @param value new {@code String} value to be set. 55 | */ 56 | public void setValue(@NonNull String value) { 57 | this.value = value; 58 | } 59 | 60 | @Override 61 | public void write(DataOutput output, int depth, TagTypeRegistry registry) throws IOException { 62 | output.writeUTF(this.value); 63 | } 64 | 65 | @Override 66 | public StringTag read(DataInput input, int depth, TagTypeRegistry registry) throws IOException { 67 | this.value = input.readUTF(); 68 | 69 | return this; 70 | } 71 | 72 | @Override 73 | public String toSnbt(int depth, TagTypeRegistry registry, SnbtConfig config) { 74 | return StringUtils.escapeSnbt(this.value); 75 | } 76 | 77 | @Override 78 | public JsonObject toJson(int depth, TagTypeRegistry registry) { 79 | JsonObject json = new JsonObject(); 80 | json.addProperty("type", this.getTypeId()); 81 | 82 | if (this.getName() != null) { 83 | json.addProperty("name", this.getName()); 84 | } 85 | 86 | json.addProperty("value", this.value); 87 | 88 | return json; 89 | } 90 | 91 | @Override 92 | public StringTag fromJson(JsonObject json, int depth, TagTypeRegistry registry) { 93 | if (json.has("name")) { 94 | this.setName(json.getAsJsonPrimitive("name").getAsString()); 95 | } else { 96 | this.setName(null); 97 | } 98 | 99 | this.value = json.getAsJsonPrimitive("value").getAsString(); 100 | 101 | return this; 102 | } 103 | 104 | @Override 105 | public String toString() { 106 | return this.toSnbt(0, new TagTypeRegistry(), new SnbtConfig()); 107 | } 108 | 109 | @Override 110 | public boolean equals(Object o) { 111 | if (this == o) return true; 112 | if (o == null || getClass() != o.getClass()) return false; 113 | 114 | StringTag stringTag = (StringTag) o; 115 | 116 | return Objects.equals(value, stringTag.value); 117 | } 118 | 119 | @Override 120 | public int hashCode() { 121 | return value.hashCode(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/dev/dewy/nbt/tags/primitive/ShortTag.java: -------------------------------------------------------------------------------- 1 | package dev.dewy.nbt.tags.primitive; 2 | 3 | import com.google.gson.JsonObject; 4 | import dev.dewy.nbt.api.registry.TagTypeRegistry; 5 | import dev.dewy.nbt.api.snbt.SnbtConfig; 6 | import dev.dewy.nbt.tags.TagType; 7 | import lombok.AllArgsConstructor; 8 | import lombok.NoArgsConstructor; 9 | import lombok.NonNull; 10 | 11 | import java.io.DataInput; 12 | import java.io.DataOutput; 13 | import java.io.IOException; 14 | 15 | /** 16 | * The short tag (type ID 2) is used for storing a 16-bit signed two's complement integer; a Java primitive {@code short}. 17 | * 18 | * @author dewy 19 | */ 20 | @NoArgsConstructor 21 | @AllArgsConstructor 22 | public class ShortTag extends NumericalTag { 23 | private short value; 24 | 25 | /** 26 | * Constructs a short tag with a given value. 27 | * 28 | * @param value the tag's {@code Number} value, to be converted to {@code short}. 29 | */ 30 | public ShortTag(@NonNull Number value) { 31 | this(null, value); 32 | } 33 | 34 | /** 35 | * Constructs a short tag with a given name and value. 36 | * 37 | * @param name the tag's name. 38 | * @param value the tag's {@code Number} value, to be converted to {@code short}. 39 | */ 40 | public ShortTag(String name, @NonNull Number value) { 41 | this(name, value.shortValue()); 42 | } 43 | 44 | /** 45 | * Constructs a short tag with a given name and value. 46 | * 47 | * @param name the tag's name. 48 | * @param value the tag's {@code short} value. 49 | */ 50 | public ShortTag(String name, short value) { 51 | this.setName(name); 52 | this.setValue(value); 53 | } 54 | 55 | @Override 56 | public byte getTypeId() { 57 | return TagType.SHORT.getId(); 58 | } 59 | 60 | @Override 61 | public Short getValue() { 62 | return this.value; 63 | } 64 | 65 | /** 66 | * Sets the {@code short} value of this short tag. 67 | * 68 | * @param value new {@code short} value to be set. 69 | */ 70 | public void setValue(short value) { 71 | this.value = value; 72 | } 73 | 74 | @Override 75 | public void write(DataOutput output, int depth, TagTypeRegistry registry) throws IOException { 76 | output.writeShort(this.value); 77 | } 78 | 79 | @Override 80 | public ShortTag read(DataInput input, int depth, TagTypeRegistry registry) throws IOException { 81 | this.value = input.readShort(); 82 | 83 | return this; 84 | } 85 | 86 | @Override 87 | public String toSnbt(int depth, TagTypeRegistry registry, SnbtConfig config) { 88 | return this.value + "s"; 89 | } 90 | 91 | @Override 92 | public JsonObject toJson(int depth, TagTypeRegistry registry) { 93 | JsonObject json = new JsonObject(); 94 | json.addProperty("type", this.getTypeId()); 95 | 96 | if (this.getName() != null) { 97 | json.addProperty("name", this.getName()); 98 | } 99 | 100 | json.addProperty("value", this.value); 101 | 102 | return json; 103 | } 104 | 105 | @Override 106 | public ShortTag fromJson(JsonObject json, int depth, TagTypeRegistry registry) { 107 | if (json.has("name")) { 108 | this.setName(json.getAsJsonPrimitive("name").getAsString()); 109 | } else { 110 | this.setName(null); 111 | } 112 | 113 | this.value = json.getAsJsonPrimitive("value").getAsShort(); 114 | 115 | return this; 116 | } 117 | 118 | @Override 119 | public boolean equals(Object o) { 120 | if (this == o) return true; 121 | if (o == null || getClass() != o.getClass()) return false; 122 | 123 | ShortTag shortTag = (ShortTag) o; 124 | 125 | return value == shortTag.value; 126 | } 127 | 128 | @Override 129 | public int hashCode() { 130 | return value; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/dev/dewy/nbt/tags/primitive/ByteTag.java: -------------------------------------------------------------------------------- 1 | package dev.dewy.nbt.tags.primitive; 2 | 3 | import com.google.gson.JsonObject; 4 | import dev.dewy.nbt.api.registry.TagTypeRegistry; 5 | import dev.dewy.nbt.api.snbt.SnbtConfig; 6 | import dev.dewy.nbt.tags.TagType; 7 | import lombok.AllArgsConstructor; 8 | import lombok.NoArgsConstructor; 9 | import lombok.NonNull; 10 | 11 | import java.io.DataInput; 12 | import java.io.DataOutput; 13 | import java.io.IOException; 14 | 15 | /** 16 | * The byte tag (type ID 1) is used for storing an 8-bit signed two's complement integer; a Java primitive {@code byte}. 17 | * 18 | * @author dewy 19 | */ 20 | @NoArgsConstructor 21 | @AllArgsConstructor 22 | public class ByteTag extends NumericalTag { 23 | private byte value; 24 | 25 | /** 26 | * Constructs a byte tag with a given value. 27 | * 28 | * @param value the tag's {@code Number} value, to be converted to {@code byte}. 29 | */ 30 | public ByteTag(@NonNull Number value) { 31 | this(null, value); 32 | } 33 | 34 | /** 35 | * Constructs a byte tag with a given name and value. 36 | * 37 | * @param name the tag's name. 38 | * @param value the tag's {@code Number} value, to be converted to {@code byte}. 39 | */ 40 | public ByteTag(String name, @NonNull Number value) { 41 | this(name, value.byteValue()); 42 | } 43 | 44 | /** 45 | * Constructs a byte tag with a given name and value. 46 | * 47 | * @param name the tag's name. 48 | * @param value the tag's {@code byte} value. 49 | */ 50 | public ByteTag(String name, byte value) { 51 | this.setName(name); 52 | this.setValue(value); 53 | } 54 | 55 | @Override 56 | public byte getTypeId() { 57 | return TagType.BYTE.getId(); 58 | } 59 | 60 | @Override 61 | public Byte getValue() { 62 | return this.value; 63 | } 64 | 65 | /** 66 | * Sets the {@code byte} value of this byte tag. 67 | * 68 | * @param value new {@code byte} value to be set. 69 | */ 70 | public void setValue(byte value) { 71 | this.value = value; 72 | } 73 | 74 | @Override 75 | public void write(DataOutput output, int depth, TagTypeRegistry registry) throws IOException { 76 | output.writeByte(this.value); 77 | } 78 | 79 | @Override 80 | public ByteTag read(DataInput input, int depth, TagTypeRegistry registry) throws IOException { 81 | this.value = input.readByte(); 82 | 83 | return this; 84 | } 85 | 86 | @Override 87 | public String toSnbt(int depth, TagTypeRegistry registry, SnbtConfig config) { 88 | return this.value + "b"; 89 | } 90 | 91 | @Override 92 | public JsonObject toJson(int depth, TagTypeRegistry registry) throws IOException { 93 | JsonObject json = new JsonObject(); 94 | json.addProperty("type", this.getTypeId()); 95 | 96 | if (this.getName() != null) { 97 | json.addProperty("name", this.getName()); 98 | } 99 | 100 | json.addProperty("value", this.value); 101 | 102 | return json; 103 | } 104 | 105 | @Override 106 | public ByteTag fromJson(JsonObject json, int depth, TagTypeRegistry registry) throws IOException { 107 | if (json.has("name")) { 108 | this.setName(json.getAsJsonPrimitive("name").getAsString()); 109 | } else { 110 | this.setName(null); 111 | } 112 | 113 | this.value = json.getAsJsonPrimitive("value").getAsByte(); 114 | 115 | return this; 116 | } 117 | 118 | @Override 119 | public boolean equals(Object o) { 120 | if (this == o) return true; 121 | if (o == null || getClass() != o.getClass()) return false; 122 | 123 | ByteTag byteTag = (ByteTag) o; 124 | 125 | return value == byteTag.value; 126 | } 127 | 128 | @Override 129 | public int hashCode() { 130 | return value; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/java/dev/dewy/nbt/api/registry/TagTypeRegistry.java: -------------------------------------------------------------------------------- 1 | package dev.dewy.nbt.api.registry; 2 | 3 | import dev.dewy.nbt.api.Tag; 4 | import dev.dewy.nbt.tags.TagType; 5 | import lombok.NonNull; 6 | 7 | import java.lang.reflect.Constructor; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | /** 12 | * A registry mapping {@code byte} tag type IDs to tag type classes. Used to register custom-made {@link Tag} types. 13 | * 14 | * @author dewy 15 | */ 16 | public class TagTypeRegistry { 17 | private final Map> registry = new HashMap<>(); 18 | 19 | { 20 | TagType.registerAll(this); 21 | } 22 | 23 | /** 24 | * Register a custom-made tag type with a unique {@code byte} ID. IDs 0-12 (inclusive) are reserved and may not be used. 25 | * 26 | * @param id the tag type's unique ID used in reading and writing. 27 | * @param clazz the tag type class. 28 | * @throws TagTypeRegistryException if the ID provided is either registered already or is a reserved ID (0-12 inclusive). 29 | */ 30 | public void registerTagType(byte id, @NonNull Class clazz) throws TagTypeRegistryException { 31 | if (id == 0) { 32 | throw new TagTypeRegistryException("Cannot register NBT tag type " + clazz + " with ID " + id + ", as that ID is reserved."); 33 | } 34 | 35 | if (this.registry.containsKey(id)) { 36 | throw new TagTypeRegistryException("Cannot register NBT tag type " + clazz + " with ID " + id + ", as that ID is already in use by the tag type " + this.registry.get(id).getSimpleName()); 37 | } 38 | 39 | if (registry.containsValue(clazz)) { 40 | byte existing = 0; 41 | for (Map.Entry> entry : this.registry.entrySet()) { 42 | if (entry.getValue().equals(clazz)) { 43 | existing = entry.getKey(); 44 | } 45 | } 46 | 47 | throw new TagTypeRegistryException("NBT tag type " + clazz.getSimpleName() + " already registered under ID " + existing); 48 | } 49 | 50 | this.registry.put(id, clazz); 51 | } 52 | 53 | /** 54 | * Deregister a custom-made tag type with a provided tag type ID. 55 | * 56 | * @param id the ID of the tag type to deregister. 57 | * @return if the tag type was deregistered successfully. 58 | */ 59 | public boolean deregisterTagType(byte id) { 60 | if (id >= 0 && id <= 12) { 61 | return false; 62 | } 63 | 64 | return this.registry.remove(id) != null; 65 | } 66 | 67 | /** 68 | * Deregister a custom-made tag type with a provided tag type ID and class value. 69 | * 70 | * @param id the ID of the tag type to deregister. 71 | * @param clazz the class value of the tag type to deregister. 72 | * @return if the tag type was deregistered successfully. 73 | */ 74 | public boolean deregisterTagType(byte id, Class clazz) { 75 | return this.registry.remove(id, clazz); 76 | } 77 | 78 | /** 79 | * Returns a tag type class value from the registry from a provided {@code byte} ID. 80 | * 81 | * @param id the ID of the tag type to retrieve. 82 | * @return a tag type class value from the registry from a provided {@code byte} ID. 83 | */ 84 | public Class getClassFromId(byte id) { 85 | return this.registry.get(id); 86 | } 87 | 88 | /** 89 | * Returns an empty instance of the given {@link Tag} type, with a {@code null} name and a default (possibly {@code null}) value. 90 | * Only use this if you really know what you're doing. 91 | * 92 | * @param clazz the tag type to instantiate. 93 | * @return an empty instance of the tag type provided. 94 | * @throws TagTypeRegistryException if a reflection error occurs when instantiating the tag. 95 | */ 96 | public Tag instantiate(@NonNull Class clazz) throws TagTypeRegistryException { 97 | try { 98 | Constructor constructor = clazz.getDeclaredConstructor(); 99 | constructor.setAccessible(true); 100 | 101 | return constructor.newInstance(); 102 | } catch (ReflectiveOperationException e) { 103 | throw new TagTypeRegistryException("Instance of tag type class " + clazz.getSimpleName() + " could not be created.", e); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/test/java/dev/dewy/nbt/test/NbtTest.java: -------------------------------------------------------------------------------- 1 | package dev.dewy.nbt.test; 2 | 3 | import dev.dewy.nbt.Nbt; 4 | import dev.dewy.nbt.io.CompressionType; 5 | import dev.dewy.nbt.tags.array.ByteArrayTag; 6 | import dev.dewy.nbt.tags.array.IntArrayTag; 7 | import dev.dewy.nbt.tags.array.LongArrayTag; 8 | import dev.dewy.nbt.tags.collection.CompoundTag; 9 | import dev.dewy.nbt.tags.collection.ListTag; 10 | import dev.dewy.nbt.tags.primitive.*; 11 | 12 | import java.io.File; 13 | import java.io.IOException; 14 | import java.util.ArrayList; 15 | import java.util.LinkedList; 16 | import java.util.List; 17 | 18 | /** 19 | * A simple demonstration of how the NBT library may be used. 20 | * 21 | * @author dewy 22 | */ 23 | public class NbtTest { 24 | // paths for the sample files 25 | private static final String SAMPLES_PATH = "samples/"; 26 | private static final File STANDARD_SAMPLE = new File(SAMPLES_PATH + "sample.nbt"); 27 | private static final File GZIP_SAMPLE = new File(SAMPLES_PATH + "samplegzip.nbt"); 28 | private static final File ZLIB_SAMPLE = new File(SAMPLES_PATH + "samplezlib.nbt"); 29 | private static final File JSON_SAMPLE = new File(SAMPLES_PATH + "sample.json"); 30 | 31 | // instance of the Nbt class, globally used. use setTypeRegistry() to use custom-made tag types. 32 | private static final Nbt NBT = new Nbt(); 33 | 34 | public static void main(String[] args) throws IOException { 35 | // creation of a root compound (think of it like a JSONObject in GSON) 36 | CompoundTag root = new CompoundTag("root"); 37 | 38 | // primitive NBT tags (tags contained inside compounds MUST have unique names) 39 | root.put(new ByteTag("byte", 45)); 40 | root.put(new ShortTag("short", 345)); 41 | root.put(new IntTag("int", -981735)); 42 | root.put(new LongTag("long", -398423290489L)); 43 | 44 | // more primitives, using the specialized put methods 45 | root.putFloat("float", 12.5F); 46 | root.putDouble("double", -19040912.1235); 47 | 48 | // putting a previously unnamed tag. 49 | root.put("string", new StringTag("https://dewy.dev")); 50 | 51 | // array NBT tags 52 | root.put(new ByteArrayTag("bytes", new byte[] {0, -124, 13, -6, Byte.MAX_VALUE})); 53 | root.put(new IntArrayTag("ints", new int[] {0, -1348193, 817519, Integer.MIN_VALUE, 4})); 54 | 55 | // constructing array tags with List<> objects 56 | List longList = new ArrayList<>(); 57 | longList.add(12490812L); 58 | longList.add(903814091904L); 59 | longList.add(-3L); 60 | longList.add(Long.MIN_VALUE); 61 | longList.add(Long.MAX_VALUE); 62 | longList.add(0L); 63 | 64 | root.put(new LongArrayTag("longs", longList)); 65 | 66 | // compound and list tags 67 | CompoundTag subCompound = new CompoundTag("sub"); 68 | ListTag doubles = new ListTag<>("listmoment"); 69 | 70 | for (int i = 0; i < 1776; i++) { 71 | CompoundTag tmp = new CompoundTag("tmp" + i); 72 | 73 | tmp.put(new DoubleTag("i", i)); 74 | tmp.put(new DoubleTag("n", i / 1348.1)); 75 | 76 | doubles.add(tmp); 77 | } 78 | 79 | subCompound.put(doubles); 80 | root.put(subCompound); 81 | 82 | // compound containing an empty compound 83 | ListTag compounds = new ListTag<>("compounds"); 84 | compounds.add(new CompoundTag()); 85 | root.put(compounds); 86 | 87 | // list containing an empty list of ints 88 | ListTag> listsOfInts = new ListTag<>("listofints"); 89 | listsOfInts.add(new ListTag<>()); 90 | root.putList("listofints", listsOfInts.getValue()); 91 | 92 | // writing to file (no compression type provided for no compression) 93 | NBT.toFile(root, STANDARD_SAMPLE); 94 | NBT.toFile(root, GZIP_SAMPLE, CompressionType.GZIP); 95 | NBT.toFile(root, ZLIB_SAMPLE, CompressionType.ZLIB); 96 | 97 | // displaying a Base64 representation 98 | System.out.println(NBT.toBase64(root)); 99 | 100 | // reading from file 101 | CompoundTag clone = NBT.fromFile(ZLIB_SAMPLE); 102 | System.out.println(clone.equals(root)); 103 | 104 | // retrieving data from the read compound 105 | System.out.println(clone.getName()); 106 | System.out.println("Be sure to visit " + clone.getString("string").getValue() + " c:"); 107 | 108 | // nbt to json and back: see readme for NBT JSON format documentation 109 | jsonTest(); 110 | 111 | // displaying as SNBT 112 | System.out.println(root); 113 | } 114 | 115 | private static void jsonTest() throws IOException { 116 | CompoundTag root = new CompoundTag("root"); 117 | 118 | root.putInt("primitive", 3); 119 | root.putIntArray("array", new int[]{0, 1, 2, 3}); 120 | 121 | List list = new LinkedList<>(); 122 | list.add(new StringTag("duck")); 123 | list.add(new StringTag("goose")); 124 | 125 | root.putList("list", list); 126 | root.put("compound", new CompoundTag()); 127 | 128 | NBT.toJson(root, JSON_SAMPLE); 129 | System.out.println(NBT.fromJson(JSON_SAMPLE).equals(root)); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | nbt 3 |
4 |

5 | 6 |

Flexible and intuitive library for reading and writing Minecraft's NBT format.

7 | 8 |

9 | Overview 10 | • 11 | Usage 12 | • 13 | Features 14 | • 15 | Javadocs 16 | • 17 | License 18 |

19 | 20 | ### Overview 21 | 22 | NBT (Named Binary Tag) is a binary format devised by Notch to be Minecraft's primary means of data storage. 23 | 24 | Due to the scuffed way the NBT format works, you'll probably want to read up on it before using this library in production. 25 | There are 12 types of tag that can be used in a complaint NBT file (see the links below for descriptions of each): 26 | 27 | - [Byte](src/main/java/dev/dewy/nbt/tags/primitive/ByteTag.java) 28 | - [Short](src/main/java/dev/dewy/nbt/tags/primitive/ShortTag.java) 29 | - [Int](src/main/java/dev/dewy/nbt/tags/primitive/IntTag.java) 30 | - [Long](src/main/java/dev/dewy/nbt/tags/primitive/LongTag.java) 31 | - [Float](src/main/java/dev/dewy/nbt/tags/primitive/FloatTag.java) 32 | - [Double](src/main/java/dev/dewy/nbt/tags/primitive/DoubleTag.java) 33 | - [Byte Array](src/main/java/dev/dewy/nbt/tags/array/ByteArrayTag.java) 34 | - [Int Array](src/main/java/dev/dewy/nbt/tags/array/IntArrayTag.java) 35 | - [Long Array](src/main/java/dev/dewy/nbt/tags/array/LongArrayTag.java) 36 | - [String](src/main/java/dev/dewy/nbt/tags/primitive/StringTag.java) 37 | - [List](src/main/java/dev/dewy/nbt/tags/collection/ListTag.java) 38 | - [Compound](src/main/java/dev/dewy/nbt/tags/collection/CompoundTag.java) 39 | 40 | Tag type IDs 1-12 for each of these can be found [here](src/main/java/dev/dewy/nbt/tags/TagType.java). 41 | 42 | All valid NBT structures begin with a compound tag; the root compound. Everything else in the NBT structure is a child of this root compound. 43 | 44 | The original [NBT specification](NBT.txt) written by Notch is also hosted here. 45 | 46 | ### Usage 47 | 48 | This library can be added as a dependency via maven central: 49 | 50 | ```groovy 51 | repositories { 52 | mavenCentral() 53 | } 54 | 55 | dependencies { 56 | implementation "dev.dewy:nbt:1.5.1" 57 | } 58 | ``` 59 | 60 | #### `Nbt` Sample: Base64 61 | 62 | The [Nbt](src/main/java/dev/dewy/nbt/Nbt.java) class can be used to easily (de)serialize NBT data: 63 | 64 | ```java 65 | public static final Nbt NBT = new Nbt(); 66 | ``` 67 | 68 | ```java 69 | CompoundTag test = NBT.fromBase64("CgALaGVsbG8gd29ybGQDAAR0ZXN0AAAAAAA"); 70 | 71 | System.out.println(test.getName()); // hello world 72 | System.out.println(test.getInt("test").getValue()); // 0 73 | ``` 74 | 75 | See the [NbtTest](src/test/java/dev/dewy/nbt/test/NbtTest.java) class for full sample usage. 76 | 77 | 78 | #### SNBT Format 79 | 80 | SNBT (Stringified NBT) is a format defined by Mojang used to record NBT tags as readable strings, used in command blocks. 81 | 82 | The [JSON NBT sample](samples/sample.json) encoded as SNBT is as follows: 83 | 84 | ```text 85 | {"primitive":3,"array":[I;0,1,2,3],"list":["duck","goose"],"compound":{}} 86 | ``` 87 | 88 | #### NBT JSON Format 89 | 90 | Every tag in NBT JSON is represented as an object containing the following properties: 91 | 92 | - `type` The tag's type ID. (*byte*) 93 | - `value` The tag's value. (*JSON primitive, array, or object depending on the tag's type*) 94 | - `name` The tag's name, omitted if contained inside a [list tag](src/main/java/dev/dewy/nbt/tags/collection/ListTag.java). (*string*) 95 | 96 | The [JSON NBT sample](samples/sample.json) is documented below: 97 | 98 | ```js 99 | { // root compound tag named "root" 100 | "type": 10, // compound tag type ID 101 | "name": "root", 102 | "value": { 103 | "primitive": { // an int tag named "primitive" with the value 3 104 | "type": 3, // int tag type ID 105 | "name": "primitive", 106 | "value": 3 107 | }, 108 | "array": { // an int array tag named "array" with the value [0, 1, 2, 3] 109 | "type": 11, // int array tag type ID 110 | "name": "array", 111 | "value": [ 112 | 0, 113 | 1, 114 | 2, 115 | 3 116 | ] 117 | }, 118 | "list": { // a list tag named "list" containing string tags. 119 | "type": 9, // list tag type ID 120 | "listType": 8, // string tag type ID (this list contains string tags) 121 | "name": "list", 122 | "value": [ 123 | { // unnamed string tag contained within "list" 124 | "type": 8, // string tag type ID 125 | "value": "duck" 126 | }, 127 | { 128 | "type": 8, 129 | "value": "goose" 130 | } 131 | ] 132 | }, 133 | "compound": { // an empty compound tag named "compound" 134 | "type": 10, // compound tag type ID 135 | "name": "compound", 136 | "value": {} // empty 137 | } 138 | } 139 | } 140 | ``` 141 | 142 | ### Features 143 | 144 | - Fully compliant with Mojang's "standards" 145 | - Small and lightweight (40Kb!) 146 | - Supports all Java edition NBT tags (including long array) 147 | - Intuitive and flexible reading and writing functionality 148 | - JSON (De)serialization 149 | - SNBT Serialization 150 | 151 | ### Javadocs 152 | 153 | Javadocs for the library can be found [here](https://javadoc.io/doc/dev.dewy/nbt/latest/index.html). 154 | 155 | ### License 156 | 157 | This project is licensed under the MIT license: see [here](LICENSE.md). 158 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### NBT ### 2 | 3 | *.nbt 4 | *.nbtt 5 | 6 | !samples/sample.nbt 7 | !samples/samplegzip.nbt 8 | !samples/samplezlib.nbt 9 | 10 | ### Eclipse ### 11 | .metadata 12 | bin/ 13 | tmp/ 14 | *.tmp 15 | *.bak 16 | *.swp 17 | *~.nib 18 | local.properties 19 | .settings/ 20 | .loadpath 21 | .recommenders 22 | 23 | # External tool builders 24 | .externalToolBuilders/ 25 | 26 | # Locally stored "Eclipse launch configurations" 27 | *.launch 28 | 29 | # PyDev specific (Python IDE for Eclipse) 30 | *.pydevproject 31 | 32 | # CDT-specific (C/C++ Development Tooling) 33 | .cproject 34 | 35 | # CDT- autotools 36 | .autotools 37 | 38 | # Java annotation processor (APT) 39 | .factorypath 40 | 41 | # PDT-specific (PHP Development Tools) 42 | .buildpath 43 | 44 | # sbteclipse plugin 45 | .target 46 | 47 | # Tern plugin 48 | .tern-project 49 | 50 | # TeXlipse plugin 51 | .texlipse 52 | 53 | # STS (Spring Tool Suite) 54 | .springBeans 55 | 56 | # Code Recommenders 57 | .recommenders/ 58 | 59 | # Annotation Processing 60 | .apt_generated/ 61 | .apt_generated_test/ 62 | 63 | # Scala IDE specific (Scala & Java development for Eclipse) 64 | .cache-main 65 | .scala_dependencies 66 | .worksheet 67 | 68 | # Uncomment this line if you wish to ignore the project description file. 69 | # Typically, this file would be tracked if it contains build/dependency configurations: 70 | #.project 71 | 72 | ### Eclipse Patch ### 73 | # Spring Boot Tooling 74 | .sts4-cache/ 75 | 76 | ### Intellij+all ### 77 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 78 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 79 | 80 | # User-specific stuff 81 | .idea/**/workspace.xml 82 | .idea/**/tasks.xml 83 | .idea/**/usage.statistics.xml 84 | .idea/**/dictionaries 85 | .idea/**/shelf 86 | 87 | # Generated files 88 | .idea/**/contentModel.xml 89 | 90 | # Sensitive or high-churn files 91 | .idea/**/dataSources/ 92 | .idea/**/dataSources.ids 93 | .idea/**/dataSources.local.xml 94 | .idea/**/sqlDataSources.xml 95 | .idea/**/dynamic.xml 96 | .idea/**/uiDesigner.xml 97 | .idea/**/dbnavigator.xml 98 | 99 | # Gradle 100 | .idea/**/gradle.xml 101 | .idea/**/libraries 102 | 103 | # Gradle and Maven with auto-import 104 | # When using Gradle or Maven with auto-import, you should exclude module files, 105 | # since they will be recreated, and may cause churn. Uncomment if using 106 | # auto-import. 107 | # .idea/artifacts 108 | # .idea/compiler.xml 109 | # .idea/jarRepositories.xml 110 | # .idea/modules.xml 111 | # .idea/*.iml 112 | # .idea/modules 113 | # *.iml 114 | # *.ipr 115 | 116 | # CMake 117 | cmake-build-*/ 118 | 119 | # Mongo Explorer plugin 120 | .idea/**/mongoSettings.xml 121 | 122 | # File-based project format 123 | *.iws 124 | 125 | # IntelliJ 126 | out/ 127 | 128 | # mpeltonen/sbt-idea plugin 129 | .idea_modules/ 130 | 131 | # JIRA plugin 132 | atlassian-ide-plugin.xml 133 | 134 | # Cursive Clojure plugin 135 | .idea/replstate.xml 136 | 137 | # Crashlytics plugin (for Android Studio and IntelliJ) 138 | com_crashlytics_export_strings.xml 139 | crashlytics.properties 140 | crashlytics-build.properties 141 | fabric.properties 142 | 143 | # Editor-based Rest Client 144 | .idea/httpRequests 145 | 146 | # Android studio 3.1+ serialized cache file 147 | .idea/caches/build_file_checksums.ser 148 | 149 | ### Intellij+all Patch ### 150 | # Ignores the whole .idea folder and all .iml files 151 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 152 | 153 | .idea/ 154 | 155 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 156 | 157 | *.iml 158 | modules.xml 159 | .idea/misc.xml 160 | *.ipr 161 | 162 | # Sonarlint plugin 163 | .idea/sonarlint 164 | 165 | ### Java ### 166 | # Compiled class file 167 | *.class 168 | 169 | # Log file 170 | *.log 171 | 172 | # BlueJ files 173 | *.ctxt 174 | 175 | # Mobile Tools for Java (J2ME) 176 | .mtj.tmp/ 177 | 178 | # Package Files # 179 | *.jar 180 | *.war 181 | *.nar 182 | *.ear 183 | *.zip 184 | *.tar.gz 185 | *.rar 186 | 187 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 188 | hs_err_pid* 189 | 190 | ### Linux ### 191 | *~ 192 | 193 | # temporary files which can be created if a process still has a handle open of a deleted file 194 | .fuse_hidden* 195 | 196 | # KDE directory preferences 197 | .directory 198 | 199 | # Linux trash folder which might appear on any partition or disk 200 | .Trash-* 201 | 202 | # .nfs files are created when an open file is removed but is still being accessed 203 | .nfs* 204 | 205 | ### macOS ### 206 | # General 207 | .DS_Store 208 | .AppleDouble 209 | .LSOverride 210 | 211 | # Icon must end with two \r 212 | Icon 213 | 214 | 215 | # Thumbnails 216 | ._* 217 | 218 | # Files that might appear in the root of a volume 219 | .DocumentRevisions-V100 220 | .fseventsd 221 | .Spotlight-V100 222 | .TemporaryItems 223 | .Trashes 224 | .VolumeIcon.icns 225 | .com.apple.timemachine.donotpresent 226 | 227 | # Directories potentially created on remote AFP share 228 | .AppleDB 229 | .AppleDesktop 230 | Network Trash Folder 231 | Temporary Items 232 | .apdisk 233 | 234 | ### Windows ### 235 | # Windows thumbnail cache files 236 | Thumbs.db 237 | Thumbs.db:encryptable 238 | ehthumbs.db 239 | ehthumbs_vista.db 240 | 241 | # Dump file 242 | *.stackdump 243 | 244 | # Folder config file 245 | [Dd]esktop.ini 246 | 247 | # Recycle Bin used on file shares 248 | $RECYCLE.BIN/ 249 | 250 | # Windows Installer files 251 | *.cab 252 | *.msi 253 | *.msix 254 | *.msm 255 | *.msp 256 | 257 | # Windows shortcuts 258 | *.lnk 259 | 260 | ### Gradle ### 261 | .gradle 262 | build/ 263 | 264 | # Ignore Gradle GUI config 265 | gradle-app.setting 266 | 267 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 268 | !gradle-wrapper.jar 269 | 270 | # Cache of project 271 | .gradletasknamecache 272 | 273 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 274 | # gradle/wrapper/gradle-wrapper.properties 275 | 276 | ### Gradle Patch ### 277 | **/build/ 278 | -------------------------------------------------------------------------------- /NBT.txt: -------------------------------------------------------------------------------- 1 | Named Binary Tag specification 2 | 3 | NBT (Named Binary Tag) is a tag based binary format designed to carry large amounts of binary data with smaller amounts of additional data. 4 | An NBT file consists of a single GZIPped Named Tag of type TAG_Compound. 5 | 6 | A Named Tag has the following format: 7 | 8 | byte tagType 9 | TAG_String name 10 | [payload] 11 | 12 | The tagType is a single byte defining the contents of the payload of the tag. 13 | 14 | The name is a descriptive name, and can be anything (eg "cat", "banana", "Hello World!"). It has nothing to do with the tagType. 15 | The purpose for this name is to name tags so parsing is easier and can be made to only look for certain recognized tag names. 16 | Exception: If tagType is TAG_End, the name is skipped and assumed to be "". 17 | 18 | The [payload] varies by tagType. 19 | 20 | Note that ONLY Named Tags carry the name and tagType data. Explicitly identified Tags (such as TAG_String above) only contains the payload. 21 | 22 | 23 | The tag types and respective payloads are: 24 | 25 | TYPE: 0 NAME: TAG_End 26 | Payload: None. 27 | Note: This tag is used to mark the end of a list. 28 | Cannot be named! If type 0 appears where a Named Tag is expected, the name is assumed to be "". 29 | (In other words, this Tag is always just a single 0 byte when named, and nothing in all other cases) 30 | 31 | TYPE: 1 NAME: TAG_Byte 32 | Payload: A single signed byte (8 bits) 33 | 34 | TYPE: 2 NAME: TAG_Short 35 | Payload: A signed short (16 bits, big endian) 36 | 37 | TYPE: 3 NAME: TAG_Int 38 | Payload: A signed short (32 bits, big endian) 39 | 40 | TYPE: 4 NAME: TAG_Long 41 | Payload: A signed long (64 bits, big endian) 42 | 43 | TYPE: 5 NAME: TAG_Float 44 | Payload: A floating point value (32 bits, big endian, IEEE 754-2008, binary32) 45 | 46 | TYPE: 6 NAME: TAG_Double 47 | Payload: A floating point value (64 bits, big endian, IEEE 754-2008, binary64) 48 | 49 | TYPE: 7 NAME: TAG_Byte_Array 50 | Payload: TAG_Int length 51 | An array of bytes of unspecified format. The length of this array is bytes 52 | 53 | TYPE: 8 NAME: TAG_String 54 | Payload: TAG_Short length 55 | An array of bytes defining a string in UTF-8 format. The length of this array is bytes 56 | 57 | TYPE: 9 NAME: TAG_List 58 | Payload: TAG_Byte tagId 59 | TAG_Int length 60 | A sequential list of Tags (not Named Tags), of type . The length of this array is Tags 61 | Notes: All tags share the same type. 62 | 63 | TYPE: 10 NAME: TAG_Compound 64 | Payload: A sequential list of Named Tags. This array keeps going until a TAG_End is found. 65 | TAG_End end 66 | Notes: If there's a nested TAG_Compound within this tag, that one will also have a TAG_End, so simply reading until the next TAG_End will not work. 67 | The names of the named tags have to be unique within each TAG_Compound 68 | The order of the tags is not guaranteed. 69 | 70 | 71 | 72 | 73 | 74 | Decoding example: 75 | (Use http://www.minecraft.net/docs/test.nbt to test your implementation) 76 | 77 | 78 | First we start by reading a Named Tag. 79 | After unzipping the stream, the first byte is a 10. That means the tag is a TAG_Compound (as expected by the specification). 80 | 81 | The next two bytes are 0 and 11, meaning the name string consists of 11 UTF-8 characters. In this case, they happen to be "hello world". 82 | That means our root tag is named "hello world". We can now move on to the payload. 83 | 84 | From the specification, we see that TAG_Compound consists of a series of Named Tags, so we read another byte to find the tagType. 85 | It happens to be an 8. The name is 4 letters long, and happens to be "name". Type 8 is TAG_String, meaning we read another two bytes to get the length, 86 | then read that many bytes to get the contents. In this case, it's "Bananrama". 87 | 88 | So now we know the TAG_Compound contains a TAG_String named "name" with the content "Bananrama" 89 | 90 | We move on to reading the next Named Tag, and get a 0. This is TAG_End, which always has an implied name of "". That means that the list of entries 91 | in the TAG_Compound is over, and indeed all of the NBT file. 92 | 93 | So we ended up with this: 94 | 95 | TAG_Compound("hello world"): 1 entries 96 | { 97 | TAG_String("name"): Bananrama 98 | } 99 | 100 | 101 | 102 | For a slightly longer test, download http://www.minecraft.net/docs/bigtest.nbt 103 | You should end up with this: 104 | 105 | TAG_Compound("Level"): 11 entries 106 | { 107 | TAG_Short("shortTest"): 32767 108 | TAG_Long("longTest"): 9223372036854775807 109 | TAG_Float("floatTest"): 0.49823147 110 | TAG_String("stringTest"): HELLO WORLD THIS IS A TEST STRING ÅÄÖ! 111 | TAG_Int("intTest"): 2147483647 112 | TAG_Compound("nested compound test"): 2 entries 113 | { 114 | TAG_Compound("ham"): 2 entries 115 | { 116 | TAG_String("name"): Hampus 117 | TAG_Float("value"): 0.75 118 | } 119 | TAG_Compound("egg"): 2 entries 120 | { 121 | TAG_String("name"): Eggbert 122 | TAG_Float("value"): 0.5 123 | } 124 | } 125 | TAG_List("listTest (long)"): 5 entries of type TAG_Long 126 | { 127 | TAG_Long: 11 128 | TAG_Long: 12 129 | TAG_Long: 13 130 | TAG_Long: 14 131 | TAG_Long: 15 132 | } 133 | TAG_Byte("byteTest"): 127 134 | TAG_List("listTest (compound)"): 2 entries of type TAG_Compound 135 | { 136 | TAG_Compound: 2 entries 137 | { 138 | TAG_String("name"): Compound tag #0 139 | TAG_Long("created-on"): 1264099775885 140 | } 141 | TAG_Compound: 2 entries 142 | { 143 | TAG_String("name"): Compound tag #1 144 | TAG_Long("created-on"): 1264099775885 145 | } 146 | } 147 | TAG_Byte_Array("byteArrayTest (the first 1000 values of (n*n*255+n*7)%100, starting with n=0 (0, 62, 34, 16, 8, ...))"): [1000 bytes] 148 | TAG_Double("doubleTest"): 0.4931287132182315 149 | } 150 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /src/main/java/dev/dewy/nbt/tags/array/ByteArrayTag.java: -------------------------------------------------------------------------------- 1 | package dev.dewy.nbt.tags.array; 2 | 3 | import com.google.gson.JsonArray; 4 | import com.google.gson.JsonObject; 5 | import dev.dewy.nbt.api.registry.TagTypeRegistry; 6 | import dev.dewy.nbt.api.snbt.SnbtConfig; 7 | import dev.dewy.nbt.tags.TagType; 8 | import dev.dewy.nbt.tags.primitive.ByteTag; 9 | import dev.dewy.nbt.utils.StringUtils; 10 | import lombok.AllArgsConstructor; 11 | import lombok.NoArgsConstructor; 12 | import lombok.NonNull; 13 | import org.apache.commons.lang3.ArrayUtils; 14 | 15 | import java.io.DataInput; 16 | import java.io.DataOutput; 17 | import java.io.IOException; 18 | import java.util.Arrays; 19 | import java.util.Iterator; 20 | import java.util.List; 21 | import java.util.Spliterator; 22 | import java.util.function.Consumer; 23 | 24 | /** 25 | * The byte array tag (type ID 7) is used for storing {@code byte[]} arrays in NBT structures. 26 | * It is not stored as a list of {@link ByteTag}s. 27 | * 28 | * @author dewy 29 | */ 30 | @NoArgsConstructor 31 | @AllArgsConstructor 32 | public class ByteArrayTag extends ArrayTag { 33 | private @NonNull byte[] value; 34 | 35 | /** 36 | * Constructs a byte array tag with a given name and value. 37 | * 38 | * @param name the tag's name. 39 | * @param value the tag's {@code byte[]} value. 40 | */ 41 | public ByteArrayTag(String name, @NonNull byte[] value) { 42 | this.setName(name); 43 | this.setValue(value); 44 | } 45 | 46 | /** 47 | * Constructs an unnamed byte array tag using a {@code List<>} object. 48 | * 49 | * @param value the tag's {@code List<>} value, to be converted to a primitive {@code byte[]} array. 50 | */ 51 | public ByteArrayTag(@NonNull List value) { 52 | this(null, value); 53 | } 54 | 55 | /** 56 | * Constructs a byte array tag with a given name, using a List object to determine its {@code byte[]} value. 57 | * 58 | * @param name the tag's name. 59 | * @param value the tag's {@code List<>} value, to be converted to a primitive {@code byte[]} array. 60 | */ 61 | public ByteArrayTag(String name, @NonNull List value) { 62 | this.setName(name); 63 | this.setValue(ArrayUtils.toPrimitive(value.toArray(new Byte[0]))); 64 | } 65 | 66 | @Override 67 | public byte getTypeId() { 68 | return TagType.BYTE_ARRAY.getId(); 69 | } 70 | 71 | @Override 72 | public byte[] getValue() { 73 | return this.value; 74 | } 75 | 76 | /** 77 | * Sets the {@code byte[]} value of this byte array tag. 78 | * 79 | * @param value new {@code byte[]} value to be set. 80 | */ 81 | public void setValue(@NonNull byte[] value) { 82 | this.value = value; 83 | } 84 | 85 | @Override 86 | public void write(DataOutput output, int depth, TagTypeRegistry registry) throws IOException { 87 | output.writeInt(this.value.length); 88 | output.write(this.value); 89 | } 90 | 91 | @Override 92 | public ByteArrayTag read(DataInput input, int depth, TagTypeRegistry registry) throws IOException { 93 | byte[] tmp = new byte[input.readInt()]; 94 | input.readFully(tmp); 95 | 96 | this.value = tmp; 97 | 98 | return this; 99 | } 100 | 101 | @Override 102 | public JsonObject toJson(int depth, TagTypeRegistry registry) throws IOException { 103 | JsonObject json = new JsonObject(); 104 | JsonArray array = new JsonArray(); 105 | json.addProperty("type", this.getTypeId()); 106 | 107 | if (this.getName() != null) { 108 | json.addProperty("name", this.getName()); 109 | } 110 | 111 | for (byte b : this) { 112 | array.add(b); 113 | } 114 | 115 | json.add("value", array); 116 | 117 | return json; 118 | } 119 | 120 | @Override 121 | public ByteArrayTag fromJson(JsonObject json, int depth, TagTypeRegistry registry) throws IOException { 122 | JsonArray array = json.getAsJsonArray("value"); 123 | 124 | if (json.has("name")) { 125 | this.setName(json.getAsJsonPrimitive("name").getAsString()); 126 | } else { 127 | this.setName(null); 128 | } 129 | 130 | this.value = new byte[array.size()]; 131 | 132 | for (int i = 0; i < array.size(); i++) { 133 | this.value[i] = array.get(i).getAsByte(); 134 | } 135 | 136 | return this; 137 | } 138 | 139 | @Override 140 | public String toSnbt(int depth, TagTypeRegistry registry, SnbtConfig config) { 141 | StringBuilder sb = new StringBuilder("[B;"); 142 | 143 | if (config.isPrettyPrint()) { 144 | if (this.value.length < config.getInlineThreshold()) { 145 | sb.append('\n').append(StringUtils.multiplyIndent(depth + 1, config)); 146 | } else { 147 | sb.append(' '); 148 | } 149 | } 150 | 151 | for (int i = 0; i < this.value.length; ++i) { 152 | if (i != 0) { 153 | if (config.isPrettyPrint()) { 154 | if (this.value.length < config.getInlineThreshold()) { 155 | sb.append(",\n").append(StringUtils.multiplyIndent(depth + 1, config)); 156 | } else { 157 | sb.append(", "); 158 | } 159 | } else { 160 | sb.append(','); 161 | } 162 | } 163 | 164 | sb.append(this.value[i]).append('B'); 165 | } 166 | 167 | if (config.isPrettyPrint() && this.value.length < config.getInlineThreshold()) { 168 | sb.append("\n").append(StringUtils.multiplyIndent(depth , config)).append(']'); 169 | } else { 170 | sb.append(']'); 171 | } 172 | 173 | return sb.toString(); 174 | } 175 | 176 | @Override 177 | public int size() { 178 | return this.value.length; 179 | } 180 | 181 | @Override 182 | public Byte get(int index) { 183 | return this.value[index]; 184 | } 185 | 186 | @Override 187 | public Byte set(int index, @NonNull Byte element) { 188 | return this.value[index] = element; 189 | } 190 | 191 | @Override 192 | public void insert(int index, @NonNull Byte... elements) { 193 | this.value = ArrayUtils.insert(index, this.value, ArrayUtils.toPrimitive(elements)); 194 | } 195 | 196 | @Override 197 | public Byte remove(int index) { 198 | Byte previous = this.value[index]; 199 | this.value = ArrayUtils.remove(this.value, index); 200 | 201 | return previous; 202 | } 203 | 204 | @Override 205 | public void clear() { 206 | this.value = new byte[0]; 207 | } 208 | 209 | @Override 210 | public Iterator iterator() { 211 | return Arrays.asList(ArrayUtils.toObject(this.value)).iterator(); 212 | } 213 | 214 | @Override 215 | public void forEach(Consumer action) { 216 | Arrays.asList(ArrayUtils.toObject(this.value)).forEach(action); 217 | } 218 | 219 | @Override 220 | public Spliterator spliterator() { 221 | return Arrays.asList(ArrayUtils.toObject(this.value)).spliterator(); 222 | } 223 | 224 | @Override 225 | public boolean equals(Object o) { 226 | if (this == o) return true; 227 | if (o == null || getClass() != o.getClass()) return false; 228 | 229 | ByteArrayTag that = (ByteArrayTag) o; 230 | 231 | return Arrays.equals(value, that.value); 232 | } 233 | 234 | @Override 235 | public int hashCode() { 236 | return Arrays.hashCode(value); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/main/java/dev/dewy/nbt/tags/array/IntArrayTag.java: -------------------------------------------------------------------------------- 1 | package dev.dewy.nbt.tags.array; 2 | 3 | import com.google.gson.JsonArray; 4 | import com.google.gson.JsonObject; 5 | import dev.dewy.nbt.api.registry.TagTypeRegistry; 6 | import dev.dewy.nbt.api.snbt.SnbtConfig; 7 | import dev.dewy.nbt.tags.TagType; 8 | import dev.dewy.nbt.tags.primitive.IntTag; 9 | import dev.dewy.nbt.utils.StringUtils; 10 | import lombok.AllArgsConstructor; 11 | import lombok.NoArgsConstructor; 12 | import lombok.NonNull; 13 | import org.apache.commons.lang3.ArrayUtils; 14 | 15 | import java.io.DataInput; 16 | import java.io.DataOutput; 17 | import java.io.IOException; 18 | import java.util.Arrays; 19 | import java.util.Iterator; 20 | import java.util.List; 21 | import java.util.Spliterator; 22 | import java.util.function.Consumer; 23 | 24 | /** 25 | * The int array tag (type ID 11) is used for storing {@code int[]} arrays in NBT structures. 26 | * It is not stored as a list of {@link IntTag}s. 27 | * 28 | * @author dewy 29 | */ 30 | @NoArgsConstructor 31 | @AllArgsConstructor 32 | public class IntArrayTag extends ArrayTag { 33 | private @NonNull int[] value; 34 | 35 | /** 36 | * Constructs an int array tag with a given name and value. 37 | * 38 | * @param name the tag's name. 39 | * @param value the tag's {@code int[]} value. 40 | */ 41 | public IntArrayTag(String name, @NonNull int[] value) { 42 | this.setName(name); 43 | this.setValue(value); 44 | } 45 | 46 | /** 47 | * Constructs an unnamed int array tag using a {@code List<>} object. 48 | * 49 | * @param value the tag's {@code List<>} value, to be converted to a primitive {@code int[]} array. 50 | */ 51 | public IntArrayTag(@NonNull List value) { 52 | this(null, value); 53 | } 54 | 55 | /** 56 | * Constructs an int array tag with a given name, using a List object to determine its {@code int[]} value. 57 | * 58 | * @param name the tag's name. 59 | * @param value the tag's {@code List<>} value, to be converted to a primitive {@code int[]} array. 60 | */ 61 | public IntArrayTag(String name, @NonNull List value) { 62 | this.setName(name); 63 | this.setValue(ArrayUtils.toPrimitive(value.toArray(new Integer[0]))); 64 | } 65 | 66 | @Override 67 | public byte getTypeId() { 68 | return TagType.INT_ARRAY.getId(); 69 | } 70 | 71 | @Override 72 | public int[] getValue() { 73 | return this.value; 74 | } 75 | 76 | /** 77 | * Sets the {@code int[]} value of this int array tag. 78 | * 79 | * @param value new {@code int[]} value to be set. 80 | */ 81 | public void setValue(@NonNull int[] value) { 82 | this.value = value; 83 | } 84 | 85 | @Override 86 | public void write(DataOutput output, int depth, TagTypeRegistry registry) throws IOException { 87 | output.writeInt(this.value.length); 88 | 89 | for (int i : this) { 90 | output.writeInt(i); 91 | } 92 | } 93 | 94 | @Override 95 | public IntArrayTag read(DataInput input, int depth, TagTypeRegistry registry) throws IOException { 96 | this.value = new int[input.readInt()]; 97 | 98 | for (int i = 0; i < this.value.length; i++) { 99 | this.value[i] = input.readInt(); 100 | } 101 | 102 | return this; 103 | } 104 | 105 | @Override 106 | public JsonObject toJson(int depth, TagTypeRegistry registry) throws IOException { 107 | JsonObject json = new JsonObject(); 108 | JsonArray array = new JsonArray(); 109 | json.addProperty("type", this.getTypeId()); 110 | 111 | if (this.getName() != null) { 112 | json.addProperty("name", this.getName()); 113 | } 114 | 115 | for (int i : this) { 116 | array.add(i); 117 | } 118 | 119 | json.add("value", array); 120 | 121 | return json; 122 | } 123 | 124 | @Override 125 | public IntArrayTag fromJson(JsonObject json, int depth, TagTypeRegistry registry) throws IOException { 126 | JsonArray array = json.getAsJsonArray("value"); 127 | 128 | if (json.has("name")) { 129 | this.setName(json.getAsJsonPrimitive("name").getAsString()); 130 | } else { 131 | this.setName(null); 132 | } 133 | 134 | this.value = new int[array.size()]; 135 | 136 | for (int i = 0; i < array.size(); i++) { 137 | this.value[i] = array.get(i).getAsInt(); 138 | } 139 | 140 | return this; 141 | } 142 | 143 | @Override 144 | public String toSnbt(int depth, TagTypeRegistry registry, SnbtConfig config) { 145 | StringBuilder sb = new StringBuilder("[I;"); 146 | 147 | if (config.isPrettyPrint()) { 148 | if (this.value.length < config.getInlineThreshold()) { 149 | sb.append('\n').append(StringUtils.multiplyIndent(depth + 1, config)); 150 | } else { 151 | sb.append(' '); 152 | } 153 | } 154 | 155 | for (int i = 0; i < this.value.length; ++i) { 156 | if (i != 0) { 157 | if (config.isPrettyPrint()) { 158 | if (this.value.length < config.getInlineThreshold()) { 159 | sb.append(",\n").append(StringUtils.multiplyIndent(depth + 1, config)); 160 | } else { 161 | sb.append(", "); 162 | } 163 | } else { 164 | sb.append(','); 165 | } 166 | } 167 | 168 | sb.append(this.value[i]); 169 | } 170 | 171 | if (config.isPrettyPrint() && this.value.length < config.getInlineThreshold()) { 172 | sb.append("\n").append(StringUtils.multiplyIndent(depth , config)).append(']'); 173 | } else { 174 | sb.append(']'); 175 | } 176 | 177 | return sb.toString(); 178 | } 179 | 180 | @Override 181 | public int size() { 182 | return this.value.length; 183 | } 184 | 185 | @Override 186 | public Integer get(int index) { 187 | return this.value[index]; 188 | } 189 | 190 | @Override 191 | public Integer set(int index, @NonNull Integer element) { 192 | return this.value[index] = element; 193 | } 194 | 195 | @Override 196 | public void insert(int index, @NonNull Integer... elements) { 197 | this.value = ArrayUtils.insert(index, this.value, ArrayUtils.toPrimitive(elements)); 198 | } 199 | 200 | @Override 201 | public Integer remove(int index) { 202 | Integer previous = this.value[index]; 203 | this.value = ArrayUtils.remove(this.value, index); 204 | 205 | return previous; 206 | } 207 | 208 | @Override 209 | public void clear() { 210 | this.value = new int[0]; 211 | } 212 | 213 | @Override 214 | public Iterator iterator() { 215 | return Arrays.asList(ArrayUtils.toObject(this.value)).iterator(); 216 | } 217 | 218 | @Override 219 | public void forEach(Consumer action) { 220 | Arrays.asList(ArrayUtils.toObject(this.value)).forEach(action); 221 | } 222 | 223 | @Override 224 | public Spliterator spliterator() { 225 | return Arrays.asList(ArrayUtils.toObject(this.value)).spliterator(); 226 | } 227 | 228 | @Override 229 | public boolean equals(Object o) { 230 | if (this == o) return true; 231 | if (o == null || getClass() != o.getClass()) return false; 232 | 233 | IntArrayTag that = (IntArrayTag) o; 234 | 235 | return Arrays.equals(value, that.value); 236 | } 237 | 238 | @Override 239 | public int hashCode() { 240 | return Arrays.hashCode(value); 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /src/main/java/dev/dewy/nbt/tags/array/LongArrayTag.java: -------------------------------------------------------------------------------- 1 | package dev.dewy.nbt.tags.array; 2 | 3 | import com.google.gson.JsonArray; 4 | import com.google.gson.JsonObject; 5 | import dev.dewy.nbt.api.registry.TagTypeRegistry; 6 | import dev.dewy.nbt.api.snbt.SnbtConfig; 7 | import dev.dewy.nbt.tags.TagType; 8 | import dev.dewy.nbt.tags.primitive.LongTag; 9 | import dev.dewy.nbt.utils.StringUtils; 10 | import lombok.AllArgsConstructor; 11 | import lombok.NoArgsConstructor; 12 | import lombok.NonNull; 13 | import org.apache.commons.lang3.ArrayUtils; 14 | 15 | import java.io.DataInput; 16 | import java.io.DataOutput; 17 | import java.io.IOException; 18 | import java.util.Arrays; 19 | import java.util.Iterator; 20 | import java.util.List; 21 | import java.util.Spliterator; 22 | import java.util.function.Consumer; 23 | 24 | /** 25 | * The long array tag (type ID 12) is used for storing {@code long[]} arrays in NBT structures. 26 | * It is not stored as a list of {@link LongTag}s. 27 | * 28 | * @author dewy 29 | */ 30 | @NoArgsConstructor 31 | @AllArgsConstructor 32 | public class LongArrayTag extends ArrayTag { 33 | private @NonNull long[] value; 34 | 35 | /** 36 | * Constructs a long array tag with a given name and value. 37 | * 38 | * @param name the tag's name. 39 | * @param value the tag's {@code long[]} value. 40 | */ 41 | public LongArrayTag(String name, @NonNull long[] value) { 42 | this.setName(name); 43 | this.setValue(value); 44 | } 45 | 46 | /** 47 | * Constructs an unnamed long array tag using a {@code List<>} object. 48 | * 49 | * @param value the tag's {@code List<>} value, to be converted to a primitive {@code long[]} array. 50 | */ 51 | public LongArrayTag(@NonNull List value) { 52 | this(null, value); 53 | } 54 | 55 | /** 56 | * Constructs a long array tag with a given name, using a List object to determine its {@code long[]} value. 57 | * 58 | * @param name the tag's name. 59 | * @param value the tag's {@code List<>} value, to be converted to a primitive {@code long[]} array. 60 | */ 61 | public LongArrayTag(String name, @NonNull List value) { 62 | this.setName(name); 63 | this.setValue(ArrayUtils.toPrimitive(value.toArray(new Long[0]))); 64 | } 65 | 66 | @Override 67 | public byte getTypeId() { 68 | return TagType.LONG_ARRAY.getId(); 69 | } 70 | 71 | @Override 72 | public long[] getValue() { 73 | return this.value; 74 | } 75 | 76 | /** 77 | * Sets the {@code long[]} value of this long array tag. 78 | * 79 | * @param value new {@code long[]} value to be set. 80 | */ 81 | public void setValue(@NonNull long[] value) { 82 | this.value = value; 83 | } 84 | 85 | @Override 86 | public void write(DataOutput output, int depth, TagTypeRegistry registry) throws IOException { 87 | output.writeInt(this.value.length); 88 | 89 | for (long l : this) { 90 | output.writeLong(l); 91 | } 92 | } 93 | 94 | @Override 95 | public LongArrayTag read(DataInput input, int depth, TagTypeRegistry registry) throws IOException { 96 | this.value = new long[input.readInt()]; 97 | 98 | for (int i = 0; i < this.value.length; i++) { 99 | this.value[i] = input.readLong(); 100 | } 101 | 102 | return this; 103 | } 104 | 105 | @Override 106 | public JsonObject toJson(int depth, TagTypeRegistry registry) throws IOException { 107 | JsonObject json = new JsonObject(); 108 | JsonArray array = new JsonArray(); 109 | json.addProperty("type", this.getTypeId()); 110 | 111 | if (this.getName() != null) { 112 | json.addProperty("name", this.getName()); 113 | } 114 | 115 | for (long l : this) { 116 | array.add(l); 117 | } 118 | 119 | json.add("value", array); 120 | 121 | return json; 122 | } 123 | 124 | @Override 125 | public LongArrayTag fromJson(JsonObject json, int depth, TagTypeRegistry registry) throws IOException { 126 | JsonArray array = json.getAsJsonArray("value"); 127 | 128 | if (json.has("name")) { 129 | this.setName(json.getAsJsonPrimitive("name").getAsString()); 130 | } else { 131 | this.setName(null); 132 | } 133 | 134 | this.value = new long[array.size()]; 135 | 136 | for (int i = 0; i < array.size(); i++) { 137 | this.value[i] = array.get(i).getAsLong(); 138 | } 139 | 140 | return this; 141 | } 142 | 143 | @Override 144 | public String toSnbt(int depth, TagTypeRegistry registry, SnbtConfig config) { 145 | StringBuilder sb = new StringBuilder("[L;"); 146 | 147 | if (config.isPrettyPrint()) { 148 | if (this.value.length < config.getInlineThreshold()) { 149 | sb.append('\n').append(StringUtils.multiplyIndent(depth + 1, config)); 150 | } else { 151 | sb.append(' '); 152 | } 153 | } 154 | 155 | for (int i = 0; i < this.value.length; ++i) { 156 | if (i != 0) { 157 | if (config.isPrettyPrint()) { 158 | if (this.value.length < config.getInlineThreshold()) { 159 | sb.append(",\n").append(StringUtils.multiplyIndent(depth + 1, config)); 160 | } else { 161 | sb.append(", "); 162 | } 163 | } else { 164 | sb.append(','); 165 | } 166 | } 167 | 168 | sb.append(this.value[i]).append('L'); 169 | } 170 | 171 | if (config.isPrettyPrint() && this.value.length < config.getInlineThreshold()) { 172 | sb.append("\n").append(StringUtils.multiplyIndent(depth , config)).append(']'); 173 | } else { 174 | sb.append(']'); 175 | } 176 | 177 | return sb.toString(); 178 | } 179 | 180 | @Override 181 | public int size() { 182 | return this.value.length; 183 | } 184 | 185 | @Override 186 | public Long get(int index) { 187 | return this.value[index]; 188 | } 189 | 190 | @Override 191 | public Long set(int index, @NonNull Long element) { 192 | return this.value[index] = element; 193 | } 194 | 195 | @Override 196 | public void insert(int index, @NonNull Long... elements) { 197 | this.value = ArrayUtils.insert(index, this.value, ArrayUtils.toPrimitive(elements)); 198 | } 199 | 200 | @Override 201 | public Long remove(int index) { 202 | Long previous = this.value[index]; 203 | this.value = ArrayUtils.remove(this.value, index); 204 | 205 | return previous; 206 | } 207 | 208 | @Override 209 | public void clear() { 210 | this.value = new long[0]; 211 | } 212 | 213 | @Override 214 | public Iterator iterator() { 215 | return Arrays.asList(ArrayUtils.toObject(this.value)).iterator(); 216 | } 217 | 218 | @Override 219 | public void forEach(Consumer action) { 220 | Arrays.asList(ArrayUtils.toObject(this.value)).forEach(action); 221 | } 222 | 223 | @Override 224 | public Spliterator spliterator() { 225 | return Arrays.asList(ArrayUtils.toObject(this.value)).spliterator(); 226 | } 227 | 228 | @Override 229 | public boolean equals(Object o) { 230 | if (this == o) return true; 231 | if (o == null || getClass() != o.getClass()) return false; 232 | 233 | LongArrayTag that = (LongArrayTag) o; 234 | 235 | return Arrays.equals(value, that.value); 236 | } 237 | 238 | @Override 239 | public int hashCode() { 240 | return Arrays.hashCode(value); 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /src/main/java/dev/dewy/nbt/Nbt.java: -------------------------------------------------------------------------------- 1 | package dev.dewy.nbt; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonObject; 5 | import dev.dewy.nbt.api.registry.TagTypeRegistry; 6 | import dev.dewy.nbt.api.snbt.SnbtConfig; 7 | import dev.dewy.nbt.io.CompressionType; 8 | import dev.dewy.nbt.io.NbtReader; 9 | import dev.dewy.nbt.io.NbtWriter; 10 | import dev.dewy.nbt.tags.collection.CompoundTag; 11 | import lombok.Cleanup; 12 | import lombok.NonNull; 13 | 14 | import java.io.*; 15 | import java.nio.charset.StandardCharsets; 16 | import java.util.Base64; 17 | import java.util.zip.DeflaterOutputStream; 18 | import java.util.zip.GZIPInputStream; 19 | import java.util.zip.GZIPOutputStream; 20 | import java.util.zip.InflaterInputStream; 21 | 22 | /** 23 | * Standard interface for reading and writing NBT data structures. 24 | * 25 | * @author dewy 26 | */ 27 | public class Nbt { 28 | private @NonNull Gson gson; 29 | private @NonNull TagTypeRegistry typeRegistry; 30 | private @NonNull SnbtConfig snbtConfig; 31 | 32 | private final @NonNull NbtWriter writer; 33 | private final @NonNull NbtReader reader; 34 | 35 | /** 36 | * Constructs an instance of this class using a default {@link TagTypeRegistry} (supporting the standard 12 tag types). 37 | */ 38 | public Nbt() { 39 | this(new TagTypeRegistry()); 40 | } 41 | 42 | /** 43 | * Constructs an instance of this class using a given {@link TagTypeRegistry}, with a default GSON instance. 44 | * 45 | * @param typeRegistry the tag type registry to be used, typically containing custom tag entries. 46 | */ 47 | public Nbt(@NonNull TagTypeRegistry typeRegistry) { 48 | this(typeRegistry, new Gson()); 49 | } 50 | 51 | /** 52 | * Constructs an instance of this class using a given {@link TagTypeRegistry}, with a default {@link SnbtConfig} instance. 53 | * 54 | * @param typeRegistry the tag type registry to be used, typically containing custom tag entries. 55 | * @param gson the GSON instance to be used. 56 | */ 57 | public Nbt(@NonNull TagTypeRegistry typeRegistry, @NonNull Gson gson) { 58 | this(typeRegistry, gson, new SnbtConfig()); 59 | } 60 | 61 | /** 62 | * Constructs an instance of this class using a given {@link TagTypeRegistry}, {@code Gson} and an {@link SnbtConfig}. 63 | * 64 | * @param typeRegistry the tag type registry to be used, typically containing custom tag entries. 65 | * @param gson the GSON instance to be used. 66 | * @param snbtConfig the SNBT config object to be used. 67 | */ 68 | public Nbt(@NonNull TagTypeRegistry typeRegistry, @NonNull Gson gson, @NonNull SnbtConfig snbtConfig) { 69 | this.typeRegistry = typeRegistry; 70 | this.gson = gson; 71 | this.snbtConfig = snbtConfig; 72 | 73 | this.writer = new NbtWriter(typeRegistry); 74 | this.reader = new NbtReader(typeRegistry); 75 | } 76 | 77 | /** 78 | * Writes the given root {@link CompoundTag} to a provided {@link DataOutput} stream. 79 | * 80 | * @param compound the NBT structure to write, contained within a {@link CompoundTag}. 81 | * @param output the stream to write to. 82 | * @throws IOException if any I/O error occurs. 83 | */ 84 | public void toStream(@NonNull CompoundTag compound, @NonNull DataOutput output) throws IOException { 85 | this.writer.toStream(compound, output); 86 | } 87 | 88 | /** 89 | * Writes the given root {@link CompoundTag} to a {@link File} with no compression. 90 | * 91 | * @param compound the NBT structure to write, contained within a {@link CompoundTag}. 92 | * @param file the file to write to. 93 | * @throws IOException if any I/O error occurs. 94 | */ 95 | public void toFile(@NonNull CompoundTag compound, @NonNull File file) throws IOException { 96 | this.toFile(compound, file, CompressionType.NONE); 97 | } 98 | 99 | /** 100 | * Writes the given root {@link CompoundTag} to a {@link File} using a certain {@link CompressionType}. 101 | * 102 | * @param compound the NBT structure to write, contained within a {@link CompoundTag}. 103 | * @param file the file to write to. 104 | * @param compression the compression to be applied. 105 | * @throws IOException if any I/O error occurs. 106 | */ 107 | public void toFile(@NonNull CompoundTag compound, @NonNull File file, @NonNull CompressionType compression) throws IOException { 108 | @Cleanup BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file)); 109 | @Cleanup DataOutputStream dos = null; 110 | 111 | switch (compression) { 112 | case NONE: 113 | dos = new DataOutputStream(bos); 114 | break; 115 | case GZIP: 116 | dos = new DataOutputStream(new GZIPOutputStream(bos)); 117 | break; 118 | case ZLIB: 119 | dos = new DataOutputStream(new DeflaterOutputStream(bos)); 120 | } 121 | 122 | this.toStream(compound, dos); 123 | } 124 | 125 | /** 126 | * Serializes the given root {@link CompoundTag} to a SNBT (Stringified NBT). 127 | * 128 | * @param compound the NBT structure to serialize to SNBT, contained within a {@link CompoundTag}. 129 | * @return the serialized SNBT string. 130 | */ 131 | public String toSnbt(@NonNull CompoundTag compound) { 132 | return compound.toSnbt(0, this.typeRegistry, this.snbtConfig); 133 | } 134 | 135 | /** 136 | * Serializes the given root {@link CompoundTag} to a JSON {@link File}. 137 | * 138 | * @param compound the NBT structure to serialize to JSON, contained within a {@link CompoundTag}. 139 | * @param file the JSON file to write to. 140 | * @throws IOException if any I/O error occurs. 141 | */ 142 | public void toJson(@NonNull CompoundTag compound, @NonNull File file) throws IOException { 143 | @Cleanup FileWriter writer = new FileWriter(file); 144 | 145 | gson.toJson(compound.toJson(0, this.typeRegistry), writer); 146 | } 147 | 148 | /** 149 | * Converts the given root {@link CompoundTag} to a {@code byte[]} array. 150 | * 151 | * @param compound the NBT structure to write, contained within a {@link CompoundTag}. 152 | * @return the resulting {@code byte[]} array. 153 | * @throws IOException if any I/O error occurs. 154 | */ 155 | public byte[] toByteArray(@NonNull CompoundTag compound) throws IOException { 156 | @Cleanup ByteArrayOutputStream baos = new ByteArrayOutputStream(); 157 | @Cleanup DataOutputStream w = new DataOutputStream(baos); 158 | 159 | this.toStream(compound, w); 160 | 161 | return baos.toByteArray(); 162 | } 163 | 164 | /** 165 | * Converts the given root {@link CompoundTag} to a Base64 encoded string. 166 | * 167 | * @param compound the NBT structure to write, contained within a {@link CompoundTag}. 168 | * @return the resulting Base64 encoded string. 169 | * @throws IOException if any I/O error occurs. 170 | */ 171 | public String toBase64(@NonNull CompoundTag compound) throws IOException { 172 | return new String(Base64.getEncoder().encode(this.toByteArray(compound)), StandardCharsets.UTF_8); 173 | } 174 | 175 | /** 176 | * Reads an NBT data structure (root {@link CompoundTag}) from a {@link DataInput} stream. 177 | * 178 | * @param input the stream to read from. 179 | * @return the root {@link CompoundTag} read from the stream. 180 | * @throws IOException if any I/O error occurs. 181 | */ 182 | public CompoundTag fromStream(@NonNull DataInput input) throws IOException { 183 | return this.reader.fromStream(input); 184 | } 185 | 186 | /** 187 | * Reads an NBT data structure (root {@link CompoundTag}) from a {@link File}. 188 | * 189 | * @param file the file to read from. 190 | * @return the root {@link CompoundTag} read from the stream. 191 | * @throws IOException if any I/O error occurs. 192 | */ 193 | public CompoundTag fromFile(@NonNull File file) throws IOException { 194 | @Cleanup BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); 195 | @Cleanup DataInputStream in = null; 196 | 197 | @Cleanup FileInputStream fis = new FileInputStream(file); 198 | switch (CompressionType.getCompression(fis)) { 199 | case NONE: 200 | in = new DataInputStream(bis); 201 | break; 202 | case GZIP: 203 | in = new DataInputStream(new GZIPInputStream(bis)); 204 | break; 205 | case ZLIB: 206 | in = new DataInputStream(new InflaterInputStream(bis)); 207 | break; 208 | default: 209 | throw new IllegalStateException("Illegal compression type. This should never happen."); 210 | } 211 | 212 | return this.fromStream(in); 213 | } 214 | 215 | /** 216 | * Deserializes an NBT data structure (root {@link CompoundTag}) from a JSON {@link File}. 217 | * 218 | * @param file the JSON file to read from. 219 | * @return the root {@link CompoundTag} deserialized from the JSON file. 220 | * @throws IOException if any I/O error occurs. 221 | */ 222 | public CompoundTag fromJson(@NonNull File file) throws IOException { 223 | @Cleanup FileReader reader = new FileReader(file); 224 | 225 | return new CompoundTag().fromJson(gson.fromJson(reader, JsonObject.class), 0, this.typeRegistry); 226 | } 227 | 228 | /** 229 | * Reads an NBT data structure (root {@link CompoundTag}) from a {@code byte[]} array. 230 | * 231 | * @param bytes the {@code byte[]} array to read from. 232 | * @return the root {@link CompoundTag} read from the stream. 233 | * @throws IOException if any I/O error occurs. 234 | */ 235 | public CompoundTag fromByteArray(@NonNull byte[] bytes) throws IOException { 236 | @Cleanup DataInputStream bais = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(bytes))); 237 | 238 | return fromStream(bais); 239 | } 240 | 241 | /** 242 | * Decodes an NBT data structure (root {@link CompoundTag}) from a Base64 encoded string. 243 | * 244 | * @param encoded the encoded Base64 string to decode. 245 | * @return the decoded root {@link CompoundTag}. 246 | * @throws IOException if any I/O error occurs. 247 | */ 248 | public CompoundTag fromBase64(@NonNull String encoded) throws IOException { 249 | return fromByteArray(Base64.getDecoder().decode(encoded)); 250 | } 251 | 252 | /** 253 | * Returns the {@link TagTypeRegistry} currently in use by this instance. 254 | * 255 | * @return the {@link TagTypeRegistry} currently in use by this instance. 256 | */ 257 | public TagTypeRegistry getTypeRegistry() { 258 | return typeRegistry; 259 | } 260 | 261 | /** 262 | * Sets the {@link TagTypeRegistry} currently in use by this instance. Used to utilise custom-made tag types. 263 | * 264 | * @param typeRegistry the new {@link TagTypeRegistry} to be set. 265 | */ 266 | public void setTypeRegistry(@NonNull TagTypeRegistry typeRegistry) { 267 | this.typeRegistry = typeRegistry; 268 | 269 | this.writer.setTypeRegistry(typeRegistry); 270 | this.reader.setTypeRegistry(typeRegistry); 271 | } 272 | 273 | /** 274 | * Returns the {@code Gson} currently in use by this instance. 275 | * 276 | * @return the {@code Gson} currently in use by this instance. 277 | */ 278 | public Gson getGson() { 279 | return gson; 280 | } 281 | 282 | /** 283 | * Sets the {@code Gson} currently in use by this instance. 284 | * 285 | * @param gson the new {@code Gson} to be set. 286 | */ 287 | public void setGson(@NonNull Gson gson) { 288 | this.gson = gson; 289 | } 290 | 291 | /** 292 | * Returns the {@link SnbtConfig} currently in use by this instance. 293 | * 294 | * @return the {@link SnbtConfig} currently in use by this instance. 295 | */ 296 | public SnbtConfig getSnbtConfig() { 297 | return snbtConfig; 298 | } 299 | 300 | /** 301 | * Sets the {@link SnbtConfig} currently in use by this instance. 302 | * 303 | * @param snbtConfig the new {@link SnbtConfig} to be set. 304 | */ 305 | public void setSnbtConfig(@NonNull SnbtConfig snbtConfig) { 306 | this.snbtConfig = snbtConfig; 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /src/main/java/dev/dewy/nbt/tags/collection/ListTag.java: -------------------------------------------------------------------------------- 1 | package dev.dewy.nbt.tags.collection; 2 | 3 | import com.google.gson.JsonArray; 4 | import com.google.gson.JsonElement; 5 | import com.google.gson.JsonObject; 6 | import dev.dewy.nbt.api.Tag; 7 | import dev.dewy.nbt.api.json.JsonSerializable; 8 | import dev.dewy.nbt.api.registry.TagTypeRegistry; 9 | import dev.dewy.nbt.api.registry.TagTypeRegistryException; 10 | import dev.dewy.nbt.api.snbt.SnbtConfig; 11 | import dev.dewy.nbt.api.snbt.SnbtSerializable; 12 | import dev.dewy.nbt.tags.TagType; 13 | import dev.dewy.nbt.utils.StringUtils; 14 | import lombok.AllArgsConstructor; 15 | import lombok.NonNull; 16 | 17 | import java.io.DataInput; 18 | import java.io.DataOutput; 19 | import java.io.IOException; 20 | import java.util.*; 21 | import java.util.function.Consumer; 22 | 23 | /** 24 | * The list tag (type ID 9) is used for storing an ordered list of unnamed NBT tags all of the same type. 25 | * 26 | * @author dewy 27 | */ 28 | @AllArgsConstructor 29 | public class ListTag extends Tag implements SnbtSerializable, JsonSerializable, Iterable { 30 | private @NonNull List value; 31 | private byte type; 32 | 33 | /** 34 | * Constructs an empty, unnamed list tag. 35 | */ 36 | public ListTag() { 37 | this(null); 38 | } 39 | 40 | /** 41 | * Constructs an empty list tag with a given name. 42 | * 43 | * @param name the tag's name. 44 | */ 45 | public ListTag(String name) { 46 | this(name, new LinkedList<>()); 47 | } 48 | 49 | /** 50 | * Constructs a list tag with a given name and {@code List<>} value. 51 | * 52 | * @param name the tag's name. 53 | * @param value the tag's {@code List<>} value. 54 | */ 55 | public ListTag(String name, @NonNull List value) { 56 | if (value.isEmpty()) { 57 | this.type = 0; 58 | } else { 59 | this.type = value.get(0).getTypeId(); 60 | } 61 | 62 | this.setName(name); 63 | this.setValue(value); 64 | } 65 | 66 | @Override 67 | public byte getTypeId() { 68 | return TagType.LIST.getId(); 69 | } 70 | 71 | @Override 72 | public List getValue() { 73 | return this.value; 74 | } 75 | 76 | /** 77 | * Returns the ID of the NBT tag type this list holds. 78 | * 79 | * @return the ID of the NBT tag type this list holds. 80 | */ 81 | public byte getListType() { 82 | return this.type; 83 | } 84 | 85 | /** 86 | * Sets the {@code List<>} value of this list tag. 87 | * 88 | * @param value new {@code List<>} value to be set. 89 | */ 90 | public void setValue(@NonNull List value) { 91 | if (value.isEmpty()) { 92 | this.type = 0; 93 | } else { 94 | this.type = value.get(0).getTypeId(); 95 | } 96 | 97 | this.value = value; 98 | } 99 | 100 | @Override 101 | public void write(DataOutput output, int depth, TagTypeRegistry registry) throws IOException { 102 | if (depth > 512) { 103 | throw new IOException("NBT structure too complex (depth > 512)."); 104 | } 105 | 106 | output.writeByte(this.type); 107 | output.writeInt(this.value.size()); 108 | 109 | for (T tag : this) { 110 | tag.write(output, depth + 1, registry); 111 | } 112 | } 113 | 114 | @Override 115 | public ListTag read(DataInput input, int depth, TagTypeRegistry registry) throws IOException { 116 | if (depth > 512) { 117 | throw new IOException("NBT structure too complex (depth > 512)."); 118 | } 119 | 120 | List tags = new ArrayList<>(); 121 | 122 | byte tagType = input.readByte(); 123 | int length = input.readInt(); 124 | 125 | T next; 126 | for (int i = 0; i < length; i++) { 127 | Class tagClass = registry.getClassFromId(tagType); 128 | 129 | if (tagClass == null) { 130 | throw new IOException("Tag type with ID " + tagType + " not present in tag type registry."); 131 | } 132 | 133 | try { 134 | next = (T) registry.instantiate(tagClass); 135 | } catch (TagTypeRegistryException e) { 136 | throw new IOException(e); 137 | } 138 | 139 | next.read(input, depth + 1, registry); 140 | next.setName(null); 141 | 142 | tags.add(next); 143 | } 144 | 145 | if (tags.isEmpty()) { 146 | this.type = 0; 147 | } else { 148 | this.type = tagType; 149 | } 150 | 151 | this.value = tags; 152 | 153 | return this; 154 | } 155 | 156 | @Override 157 | public String toSnbt(int depth, TagTypeRegistry registry, SnbtConfig config) { 158 | StringBuilder sb = new StringBuilder("["); 159 | 160 | if (config.isPrettyPrint()) { 161 | sb.append('\n').append(StringUtils.multiplyIndent(depth + 1, config)); 162 | } 163 | 164 | for (int i = 0; i < this.value.size(); ++i) { 165 | if (i != 0) { 166 | if (config.isPrettyPrint()) { 167 | sb.append(",\n").append(StringUtils.multiplyIndent(depth + 1, config)); 168 | } else { 169 | sb.append(','); 170 | } 171 | } 172 | 173 | sb.append(((SnbtSerializable) this.value.get(i)).toSnbt(depth + 1, registry, config)); 174 | } 175 | 176 | if (config.isPrettyPrint()) { 177 | sb.append("\n").append(StringUtils.multiplyIndent(depth , config)).append(']'); 178 | } else { 179 | sb.append(']'); 180 | } 181 | 182 | return sb.toString(); 183 | } 184 | 185 | @Override 186 | public JsonObject toJson(int depth, TagTypeRegistry registry) throws IOException { 187 | if (depth > 512) { 188 | throw new IOException("NBT structure too complex (depth > 512)."); 189 | } 190 | 191 | JsonObject json = new JsonObject(); 192 | JsonArray value = new JsonArray(); 193 | 194 | json.addProperty("type", this.getTypeId()); 195 | json.addProperty("listType", this.getListType()); 196 | 197 | if (this.getName() != null) { 198 | json.addProperty("name", this.getName()); 199 | } 200 | 201 | for (T tag : this) { 202 | tag.setName(null); 203 | value.add(((JsonSerializable) tag).toJson(depth + 1, registry)); 204 | } 205 | 206 | json.add("value", value); 207 | 208 | return json; 209 | } 210 | 211 | @Override 212 | public ListTag fromJson(JsonObject json, int depth, TagTypeRegistry registry) throws IOException { 213 | if (depth > 512) { 214 | throw new IOException("NBT structure too complex (depth > 512)."); 215 | } 216 | 217 | this.clear(); 218 | 219 | if (json.has("name")) { 220 | this.setName(json.getAsJsonPrimitive("name").getAsString()); 221 | } else { 222 | this.setName(null); 223 | } 224 | 225 | byte listType = json.get("listType").getAsByte(); 226 | List tags = new LinkedList<>(); 227 | 228 | T nextTag; 229 | for (JsonElement element : json.getAsJsonArray("value")) { 230 | Class tagClass = registry.getClassFromId(listType); 231 | 232 | if (tagClass == null) { 233 | throw new IOException("Tag type with ID " + listType + " not present in tag type registry."); 234 | } 235 | 236 | try { 237 | nextTag = (T) registry.instantiate(tagClass); 238 | } catch (TagTypeRegistryException e) { 239 | throw new IOException(e); 240 | } 241 | 242 | ((JsonSerializable) nextTag).fromJson((JsonObject) element, depth + 1, registry); 243 | tags.add(nextTag); 244 | } 245 | 246 | if (tags.isEmpty()) { 247 | this.type = 0; 248 | } else { 249 | this.type = listType; 250 | } 251 | 252 | this.value = tags; 253 | 254 | return this; 255 | } 256 | 257 | /** 258 | * Returns the number of elements in this list tag. 259 | * 260 | * @return the number of elements in this list tag. 261 | */ 262 | public int size() { 263 | return this.value.size(); 264 | } 265 | 266 | /** 267 | * Returns true if this list tag is empty, false otherwise. 268 | * 269 | * @return true if this list tag is empty, false otherwise. 270 | */ 271 | public boolean isEmpty() { 272 | return this.value.isEmpty(); 273 | } 274 | 275 | /** 276 | * Appends the specified tag to the end of the list. Returns true if added successfully. 277 | * 278 | * @param tag the tag to be added. 279 | * @return true if added successfully. 280 | */ 281 | public boolean add(@NonNull T tag) { 282 | if (this.value.isEmpty()) { 283 | this.type = tag.getTypeId(); 284 | } 285 | 286 | if (tag.getTypeId() != this.type) { 287 | return false; 288 | } 289 | 290 | return this.value.add(tag); 291 | } 292 | 293 | /** 294 | * Inserts the specified tag at the specified position in this list. 295 | * Shifts the tag currently at that position and any subsequent tags to the right. 296 | * 297 | * @param index index at which the tag is to be inserted. 298 | * @param tag tag to be inserted. 299 | */ 300 | public void insert(int index, @NonNull T tag) { 301 | if (this.value.isEmpty()) { 302 | this.type = tag.getTypeId(); 303 | } 304 | 305 | if (tag.getTypeId() != this.type) { 306 | return; 307 | } 308 | 309 | this.value.add(index, tag); 310 | } 311 | 312 | /** 313 | * Removes a given tag from the list. Returns true if removed successfully, false otherwise. 314 | * 315 | * @param tag the tag to be removed. 316 | * @return true if the tag was removed successfully, false otherwise. 317 | */ 318 | public boolean remove(@NonNull T tag) { 319 | boolean success = this.value.remove(tag); 320 | 321 | if (this.value.isEmpty()) { 322 | this.type = 0; 323 | } 324 | 325 | return success; 326 | } 327 | 328 | /** 329 | * Removes a tag from the list based on the tag's index. Returns the removed tag. 330 | * 331 | * @param index the index of the tag to be removed. 332 | * @return the removed tag. 333 | */ 334 | public T remove(int index) { 335 | T previous = this.value.remove(index); 336 | 337 | if (this.value.isEmpty()) { 338 | this.type = 0; 339 | } 340 | 341 | return previous; 342 | } 343 | 344 | /** 345 | * Retrieves a tag from its index in the list. 346 | * 347 | * @param index the index of the tag to be retrieved. 348 | * @return the tag at the specified index. 349 | */ 350 | public T get(int index) { 351 | return this.value.get(index); 352 | } 353 | 354 | /** 355 | * Returns true if this list contains the tag, false otherwise. 356 | * 357 | * @param tag the tag to check for. 358 | * @return true if this list contains the tag, false otherwise. 359 | */ 360 | public boolean contains(@NonNull T tag) { 361 | return this.value.contains(tag); 362 | } 363 | 364 | /** 365 | * Returns true if this list contains all tags in the collection, false otherwise. 366 | * 367 | * @param tags the tags to be checked for. 368 | * @return true if this list contains all tags in the collection, false otherwise. 369 | */ 370 | public boolean containsAll(@NonNull Collection tags) { 371 | return this.value.containsAll(tags); 372 | } 373 | 374 | /** 375 | * Removes all tags from the list. The list will be empty after this call returns. 376 | */ 377 | public void clear() { 378 | this.type = 0; 379 | this.value.clear(); 380 | } 381 | 382 | @Override 383 | public Iterator iterator() { 384 | return this.value.iterator(); 385 | } 386 | 387 | @Override 388 | public void forEach(Consumer action) { 389 | this.value.forEach(action); 390 | } 391 | 392 | @Override 393 | public Spliterator spliterator() { 394 | return this.value.spliterator(); 395 | } 396 | 397 | @Override 398 | public String toString() { 399 | return this.toSnbt(0, new TagTypeRegistry(), new SnbtConfig()); 400 | } 401 | 402 | @Override 403 | public boolean equals(Object o) { 404 | if (this == o) return true; 405 | if (o == null || getClass() != o.getClass()) return false; 406 | 407 | ListTag listTag = (ListTag) o; 408 | 409 | if (type != listTag.type) return false; 410 | return Objects.equals(value, listTag.value); 411 | } 412 | 413 | @Override 414 | public int hashCode() { 415 | int result = value != null ? value.hashCode() : 0; 416 | result = 31 * result + (int) type; 417 | return result; 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /src/main/java/dev/dewy/nbt/tags/collection/CompoundTag.java: -------------------------------------------------------------------------------- 1 | package dev.dewy.nbt.tags.collection; 2 | 3 | import com.google.gson.JsonElement; 4 | import com.google.gson.JsonObject; 5 | import dev.dewy.nbt.api.Tag; 6 | import dev.dewy.nbt.api.json.JsonSerializable; 7 | import dev.dewy.nbt.api.registry.TagTypeRegistry; 8 | import dev.dewy.nbt.api.registry.TagTypeRegistryException; 9 | import dev.dewy.nbt.api.snbt.SnbtConfig; 10 | import dev.dewy.nbt.api.snbt.SnbtSerializable; 11 | import dev.dewy.nbt.tags.TagType; 12 | import dev.dewy.nbt.tags.array.ByteArrayTag; 13 | import dev.dewy.nbt.tags.array.IntArrayTag; 14 | import dev.dewy.nbt.tags.array.LongArrayTag; 15 | import dev.dewy.nbt.tags.primitive.*; 16 | import dev.dewy.nbt.utils.StringUtils; 17 | import lombok.AllArgsConstructor; 18 | import lombok.NonNull; 19 | 20 | import java.io.DataInput; 21 | import java.io.DataOutput; 22 | import java.io.IOException; 23 | import java.util.*; 24 | import java.util.function.Consumer; 25 | 26 | /** 27 | * The compound tag (type ID 10) is used for storing an unordered map of any and all named tags. 28 | * All tags present in a compound must be given a name (key). Every valid NBT data structure is contained entirely within a "root" compound. 29 | * 30 | * @author dewy 31 | */ 32 | @AllArgsConstructor 33 | public class CompoundTag extends Tag implements SnbtSerializable, JsonSerializable, Iterable { 34 | private @NonNull Map value; 35 | 36 | /** 37 | * Constructs an empty, unnamed compound tag. 38 | */ 39 | public CompoundTag() { 40 | this(null, new LinkedHashMap<>()); 41 | } 42 | 43 | /** 44 | * Constructs an empty compound tag with a given name. 45 | * 46 | * @param name the tag's name. 47 | */ 48 | public CompoundTag(String name) { 49 | this(name, new LinkedHashMap<>()); 50 | } 51 | 52 | /** 53 | * Constructs a compound tag with a given name and {@code Map<>} value. 54 | * 55 | * @param name the tag's name. 56 | * @param value the tag's {@code Map<>} value. 57 | */ 58 | public CompoundTag(String name, @NonNull Map value) { 59 | this.setName(name); 60 | this.setValue(value); 61 | } 62 | 63 | @Override 64 | public byte getTypeId() { 65 | return TagType.COMPOUND.getId(); 66 | } 67 | 68 | @Override 69 | public Map getValue() { 70 | return this.value; 71 | } 72 | 73 | /** 74 | * Sets the {@code Map<>} value of this compound tag. 75 | * 76 | * @param value new {@code Map<>} value to be set. 77 | */ 78 | public void setValue(@NonNull Map value) { 79 | this.value = value; 80 | } 81 | 82 | @Override 83 | public void write(DataOutput output, int depth, TagTypeRegistry registry) throws IOException { 84 | if (depth > 512) { 85 | throw new IOException("NBT structure too complex (depth > 512)."); 86 | } 87 | 88 | for (Tag tag : this) { 89 | output.writeByte(tag.getTypeId()); 90 | output.writeUTF(tag.getName()); 91 | 92 | tag.write(output, depth + 1, registry); 93 | } 94 | 95 | output.writeByte(0); 96 | } 97 | 98 | @Override 99 | public CompoundTag read(DataInput input, int depth, TagTypeRegistry registry) throws IOException { 100 | if (depth > 512) { 101 | throw new IOException("NBT structure too complex (depth > 512)."); 102 | } 103 | 104 | Map tags = new LinkedHashMap<>(); 105 | 106 | byte nextTypeId; 107 | Tag nextTag; 108 | while ((nextTypeId = input.readByte()) != 0) { 109 | Class tagClass = registry.getClassFromId(nextTypeId); 110 | 111 | if (tagClass == null) { 112 | throw new IOException("Tag type with ID " + nextTypeId + " not present in tag type registry."); 113 | } 114 | 115 | try { 116 | nextTag = registry.instantiate(tagClass); 117 | } catch (TagTypeRegistryException e) { 118 | throw new IOException(e); 119 | } 120 | 121 | nextTag.setName(input.readUTF()); 122 | nextTag.read(input, depth + 1, registry); 123 | 124 | tags.put(nextTag.getName(), nextTag); 125 | } 126 | 127 | this.value = tags; 128 | 129 | return this; 130 | } 131 | 132 | @Override 133 | public JsonObject toJson(int depth, TagTypeRegistry registry) throws IOException { 134 | if (depth > 512) { 135 | throw new IOException("NBT structure too complex (depth > 512)."); 136 | } 137 | 138 | JsonObject json = new JsonObject(); 139 | JsonObject value = new JsonObject(); 140 | json.addProperty("type", this.getTypeId()); 141 | 142 | if (this.getName() != null) { 143 | json.addProperty("name", this.getName()); 144 | } 145 | 146 | for (Tag tag : this) { 147 | try { 148 | value.add(tag.getName(), ((JsonSerializable) tag).toJson(depth + 1, registry)); 149 | } catch (ClassCastException e) { 150 | throw new IOException("Tag not JsonSerializable.", e); 151 | } 152 | } 153 | 154 | json.add("value", value); 155 | 156 | return json; 157 | } 158 | 159 | @Override 160 | public CompoundTag fromJson(JsonObject json, int depth, TagTypeRegistry registry) throws IOException { 161 | if (depth > 512) { 162 | throw new IOException("NBT structure too complex (depth > 512)."); 163 | } 164 | 165 | this.clear(); 166 | 167 | if (json.has("name")) { 168 | this.setName(json.getAsJsonPrimitive("name").getAsString()); 169 | } else { 170 | this.setName(null); 171 | } 172 | 173 | Map tags = new LinkedHashMap<>(); 174 | 175 | byte nextTypeId; 176 | Tag nextTag; 177 | for (Map.Entry entry : json.getAsJsonObject("value").entrySet()) { 178 | JsonObject entryJson = entry.getValue().getAsJsonObject(); 179 | 180 | nextTypeId = entryJson.get("type").getAsByte(); 181 | Class tagClass = registry.getClassFromId(nextTypeId); 182 | 183 | if (tagClass == null) { 184 | throw new IOException("Tag type with ID " + nextTypeId + " not present in tag type registry."); 185 | } 186 | 187 | try { 188 | nextTag = registry.instantiate(tagClass); 189 | } catch (TagTypeRegistryException e) { 190 | throw new IOException(e); 191 | } 192 | 193 | ((JsonSerializable) nextTag).fromJson(entryJson, depth + 1, registry); 194 | tags.put(nextTag.getName(), nextTag); 195 | } 196 | 197 | this.value = tags; 198 | 199 | return this; 200 | } 201 | 202 | @Override 203 | public String toSnbt(int depth, TagTypeRegistry registry, SnbtConfig config) { 204 | if (this.value.isEmpty()) { 205 | return "{}"; 206 | } 207 | 208 | StringBuilder sb = new StringBuilder("{"); 209 | 210 | if (config.isPrettyPrint()) { 211 | sb.append('\n').append(StringUtils.multiplyIndent(depth + 1, config)); 212 | } 213 | 214 | boolean first = true; 215 | for (Tag tag : this) { 216 | if (!first) { 217 | if (config.isPrettyPrint()) { 218 | sb.append(",\n").append(StringUtils.multiplyIndent(depth + 1, config)); 219 | } else { 220 | sb.append(','); 221 | } 222 | } 223 | 224 | sb.append(StringUtils.escapeSnbt(tag.getName())); 225 | 226 | if (config.isPrettyPrint()) { 227 | sb.append(": "); 228 | } else { 229 | sb.append(':'); 230 | } 231 | 232 | sb.append(((SnbtSerializable) tag).toSnbt(depth + 1, registry, config)); 233 | 234 | if (first) { 235 | first = false; 236 | } 237 | } 238 | 239 | if (config.isPrettyPrint()) { 240 | sb.append("\n").append(StringUtils.multiplyIndent(depth , config)).append('}'); 241 | } else { 242 | sb.append('}'); 243 | } 244 | 245 | return sb.toString(); 246 | } 247 | 248 | /** 249 | * Returns the number of entries in this compound tag. 250 | * 251 | * @return the number of entries in this compound tag. 252 | */ 253 | public int size() { 254 | return this.value.size(); 255 | } 256 | 257 | /** 258 | * Returns true if this compound tag is empty, false otherwise. 259 | * 260 | * @return true if this compound tag is empty, false otherwise. 261 | */ 262 | public boolean isEmpty() { 263 | return this.value.isEmpty(); 264 | } 265 | 266 | /** 267 | * Adds a given tag to this compound. The tag must have a name, or NPE is thrown. 268 | * 269 | * @param tag the named tag to be added to the compound. 270 | * @param the type of an existing tag you believe you may be replacing (optional). 271 | * @return the previous value mapped with the tag's name as type E if provided, or null if there wasn't any. 272 | * @throws NullPointerException if the tag's name is null. 273 | */ 274 | public E put(@NonNull Tag tag) { 275 | return (E) this.value.put(tag.getName(), tag); 276 | } 277 | 278 | /** 279 | * Adds a given tag to this compound. Be careful, the tag's name is set to the {@code name} parameter automatically. 280 | * 281 | * @param name the tag's name (key). 282 | * @param tag the tag to be added to the compound. 283 | * @param the type of an existing tag you believe you may be replacing (optional). 284 | * @return the previous value mapped with the tag's name as type E if provided, or null if there wasn't any. 285 | */ 286 | public E put(@NonNull String name, @NonNull Tag tag) { 287 | tag.setName(name); 288 | 289 | return this.put(tag); 290 | } 291 | 292 | public void putByte(@NonNull String name, byte value) { 293 | this.put(name, new ByteTag(name, value)); 294 | } 295 | 296 | public void putShort(@NonNull String name, short value) { 297 | this.put(name, new ShortTag(name, value)); 298 | } 299 | 300 | public void putInt(@NonNull String name, int value) { 301 | this.put(name, new IntTag(name, value)); 302 | } 303 | 304 | public void putLong(@NonNull String name, long value) { 305 | this.put(name, new LongTag(name, value)); 306 | } 307 | 308 | public void putFloat(@NonNull String name, float value) { 309 | this.put(name, new FloatTag(name, value)); 310 | } 311 | 312 | public void putDouble(@NonNull String name, double value) { 313 | this.put(name, new DoubleTag(name, value)); 314 | } 315 | 316 | public void putByteArray(@NonNull String name, @NonNull byte[] value) { 317 | this.put(name, new ByteArrayTag(name, value)); 318 | } 319 | 320 | public void putString(@NonNull String name, @NonNull String value) { 321 | this.put(name, new StringTag(name, value)); 322 | } 323 | 324 | public void putList(@NonNull String name, List value) { 325 | this.put(name, new ListTag<>(name, value)); 326 | } 327 | 328 | public void putCompound(@NonNull String name, @NonNull Map value) { 329 | this.put(name, new CompoundTag(name, value)); 330 | } 331 | 332 | public void putIntArray(@NonNull String name, @NonNull int[] value) { 333 | this.put(name, new IntArrayTag(name, value)); 334 | } 335 | 336 | public void putLongArray(@NonNull String name, @NonNull long[] value) { 337 | this.put(name, new LongArrayTag(name, value)); 338 | } 339 | 340 | /** 341 | * Removes a tag from this compound with a given name (key). 342 | * 343 | * @param key the name whose mapping is to be removed from this compound. 344 | * @param the tag type you believe you are removing (optional). 345 | * @return the previous value associated with {@code key} as type T if provided. 346 | */ 347 | public T remove(@NonNull String key) { 348 | return (T) this.value.remove(key); 349 | } 350 | 351 | /** 352 | * Retrieves a tag from this compound with a given name (key). 353 | * 354 | * @param key the name whose mapping is to be retrieved from this compound. 355 | * @param the tag type you believe you are retrieving. 356 | * @return the value associated with {@code key} as type T. 357 | */ 358 | public T get(@NonNull String key) { 359 | return (T) this.value.get(key); 360 | } 361 | 362 | public ByteTag getByte(@NonNull String key) { 363 | return this.get(key); 364 | } 365 | 366 | public ShortTag getShort(@NonNull String key) { 367 | return this.get(key); 368 | } 369 | 370 | public IntTag getInt(@NonNull String key) { 371 | return this.get(key); 372 | } 373 | 374 | public LongTag getLong(@NonNull String key) { 375 | return this.get(key); 376 | } 377 | 378 | public FloatTag getFloat(@NonNull String key) { 379 | return this.get(key); 380 | } 381 | 382 | public DoubleTag getDouble(@NonNull String key) { 383 | return this.get(key); 384 | } 385 | 386 | public ByteArrayTag getByteArray(@NonNull String key) { 387 | return this.get(key); 388 | } 389 | 390 | public StringTag getString(@NonNull String key) { 391 | return this.get(key); 392 | } 393 | 394 | public ListTag getList(@NonNull String key) { 395 | return this.get(key); 396 | } 397 | 398 | public CompoundTag getCompound(@NonNull String key) { 399 | return this.get(key); 400 | } 401 | 402 | public IntArrayTag getIntArray(@NonNull String key) { 403 | return this.get(key); 404 | } 405 | 406 | public LongArrayTag getLongArray(@NonNull String key) { 407 | return this.get(key); 408 | } 409 | 410 | /** 411 | * Returns true if this compound contains an entry with a given name (key), false otherwise. 412 | * 413 | * @param key the name (key) to check for. 414 | * @return true if this compound contains an entry with a given name (key), false otherwise. 415 | */ 416 | public boolean contains(@NonNull String key) { 417 | return this.value.containsKey(key); 418 | } 419 | 420 | /** 421 | * Returns true if this compound contains an entry with a given name (key) and if that entry is of a given tag type, false otherwise. 422 | * 423 | * @param key the name (key) to check for. 424 | * @param typeId the tag type ID to test for. 425 | * @return true if this compound contains an entry with a given name (key) and if that entry is of a given tag type, false otherwise. 426 | */ 427 | public boolean contains(@NonNull String key, byte typeId) { 428 | if (!this.contains(key)) { 429 | return false; 430 | } 431 | 432 | return this.get(key).getTypeId() == typeId; 433 | } 434 | 435 | public boolean containsByte(@NonNull String key) { 436 | return this.contains(key, TagType.BYTE.getId()); 437 | } 438 | 439 | public boolean containsShort(@NonNull String key) { 440 | return this.contains(key, TagType.SHORT.getId()); 441 | } 442 | 443 | public boolean containsInt(@NonNull String key) { 444 | return this.contains(key, TagType.INT.getId()); 445 | } 446 | 447 | public boolean containsLong(@NonNull String key) { 448 | return this.contains(key, TagType.LONG.getId()); 449 | } 450 | 451 | public boolean containsFloat(@NonNull String key) { 452 | return this.contains(key, TagType.FLOAT.getId()); 453 | } 454 | 455 | public boolean containsDouble(@NonNull String key) { 456 | return this.contains(key, TagType.DOUBLE.getId()); 457 | } 458 | 459 | public boolean containsByteArray(@NonNull String key) { 460 | return this.contains(key, TagType.BYTE_ARRAY.getId()); 461 | } 462 | 463 | public boolean containsString(@NonNull String key) { 464 | return this.contains(key, TagType.STRING.getId()); 465 | } 466 | 467 | public boolean containsList(@NonNull String key) { 468 | return this.contains(key, TagType.LIST.getId()); 469 | } 470 | 471 | public boolean containsListOf(@NonNull String key, byte of) { 472 | return this.containsList(key) && this.getList(key).getListType() == of; 473 | } 474 | 475 | public boolean containsCompound(@NonNull String key) { 476 | return this.contains(key, TagType.COMPOUND.getId()); 477 | } 478 | 479 | public boolean containsIntArray(@NonNull String key) { 480 | return this.contains(key, TagType.INT_ARRAY.getId()); 481 | } 482 | 483 | public boolean containsLongArray(@NonNull String key) { 484 | return this.contains(key, TagType.LONG_ARRAY.getId()); 485 | } 486 | 487 | /** 488 | * Returns all {@link Tag}s contained within this compound. 489 | * 490 | * @return all {@link Tag}s contained within this compound. 491 | */ 492 | public Collection values() { 493 | return this.value.values(); 494 | } 495 | 496 | /** 497 | * Returns a {@code Set<>} of all names (keys) currently used within this compound. 498 | * 499 | * @return a {@code Set<>} of all names (keys) currently used within this compound. 500 | */ 501 | public Set keySet() { 502 | return this.value.keySet(); 503 | } 504 | 505 | /** 506 | * Removes all entries from the compound. The compound will be empty after this call returns. 507 | */ 508 | public void clear() { 509 | this.value.clear(); 510 | } 511 | 512 | @Override 513 | public Iterator iterator() { 514 | return this.value.values().iterator(); 515 | } 516 | 517 | @Override 518 | public void forEach(Consumer action) { 519 | this.value.values().forEach(action); 520 | } 521 | 522 | @Override 523 | public Spliterator spliterator() { 524 | return this.value.values().spliterator(); 525 | } 526 | 527 | @Override 528 | public String toString() { 529 | return this.toSnbt(0, new TagTypeRegistry(), new SnbtConfig()); 530 | } 531 | 532 | @Override 533 | public boolean equals(Object o) { 534 | if (this == o) return true; 535 | if (o == null || getClass() != o.getClass()) return false; 536 | 537 | CompoundTag that = (CompoundTag) o; 538 | 539 | return Objects.equals(value, that.value); 540 | } 541 | 542 | @Override 543 | public int hashCode() { 544 | return value != null ? value.hashCode() : 0; 545 | } 546 | } 547 | --------------------------------------------------------------------------------