NBT library that provides a full compatibility with Java and Bedrock, also compatible with I/O operations, SNBT read/write, json conversion and simplified configuration format.
// Tag compound (map representation)
Map<String, Object> map = Map.of(
"someKey", "someValue",
"otherKey", 1234,
"aList", List.of(1, 2, 3, 4)
);
File file = new File("myfile.nbt");
// Write to file
try (TagOutput<Object> output = TagOutput.of(new DataOutputStream(new FileOutputStream(file)))) {
output.writeUnnamed(map);
}
// Read from file
Map<String, Object> map;
try (TagInput<Object> input = TagInput.of(new DataInputStream(new FileInputStream(file)))) {
map = input.readUnnamed();
}
// Convert to SNBT
String snbt = TagWriter.toString(map);
// Get from SNBT
Map<String, Object> map = TagReader.fromString(snbt);
// Convert to Json
JsonElement json = TagJson.toJson(map);
// Get from Json
Map<String, Object> map = TagJson.fromJson(json);
// Convert to simplified configuration
Object config = TagConfig.toConfigValue(map);
// Get from simplified configuration
Map<String, Object> map = TagConfig.fromConfigValue(config);
How to implement NBT library in your project.
build.gradle
plugins {
id 'com.gradleup.shadow' version '8.3.5'
}
repositories {
maven { url 'https://jitpack.io' }
}
dependencies {
implementation 'com.saicone:nbt:1.0'
}
jar.dependsOn (shadowJar)
shadowJar {
// Relocate nbt
relocate 'com.saicone.nbt', project.group + '.libs.nbt'
// Exclude unused classes (optional)
minimize()
}
build.gradle.kts
plugins {
id("com.gradleup.shadow") version "8.3.5"
}
repositories {
maven("https://jitpack.io")
}
dependencies {
implementation("com.saicone:nbt:1.0")
}
tasks {
jar {
dependsOn(tasks.shadowJar)
}
shadowJar {
// Relocate nbt
relocate("com.saicone.nbt", "${project.group}.libs.nbt")
// Exclude unused classes (optional)
minimize()
}
}
pom.xml
<repositories>
<repository>
<id>Jitpack</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.saicone</groupId>
<artifactId>nbt</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<relocations>
<!-- Relocate nbt -->
<relocation>
<pattern>com.saicone.nbt</pattern>
<shadedPattern>${project.groupId}.libs.nbt</shadedPattern>
</relocation>
</relocations>
<!-- Exclude unused classes (optional) -->
<minimizeJar>true</minimizeJar>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</build>
A tag mapper is a customizable tag abstraction, for example, most NBT implementations use classes like "TagString" or "NbtCompound" referring the kind of value that is handled.
With a tag mapper this library can build NBT objects as that custom implementation and extract the values from them.
This library by default is compatible with any Java object that is represented on NBT format:
- END =
null
- BYTE =
byte
- SHORT =
short
- INT =
int
- LONG =
long
- FLOAT =
float
- DOUBLE =
double
- BYTE_ARRAY =
byte[]
- STRING =
String
- LIST =
List<Object>
(Objects must be represented on NBT format) - COMPOUND =
Map<String, Object>
(Objects must be represented on NBT format) - INT_ARRAY =
int[]
- LONG_ARRAY =
long[]
You can also provide your own mapping by creating a custom implementation of TagMapper
.
Between Minecraft platforms there are important differences, creating a TagInput
or TagOutput
is not that "easy" due NBT formatting changes depending on your needs.
You should understand that "input" is when you are reading bytes and "output" is when you are writing or sending them.
We will use the following object as example:
// Tag compound (map representation)
Map<String, Object> map = Map.of(
"someKey", "someValue",
"otherKey", 1234,
"aList", List.of(1, 2, 3, 4)
);
"Java" will be referred as "Minecraft Java Edition", basically the default way to read/write NBT data on the following locations:
- Files, such as world regions, player data and also used by third-party file formats (.schematic and .schem).
- Network, sending and receiving packets with data like texts and items.
Files (go below for more information about read/write compressed data):
// Using File
OutputStream out = new FileOutputStream(new File("myfile.nbt"));
// Using Path
OutputStream out = Files.newOutputStream(Path.of("myfile.nbt"));
// Write to file
try (TagOutput<Object> output = TagOutput.of(new DataOutputStream(out))) {
output.writeUnnamed(map);
}
// Using File
InputStream in = new FileInputStream(new File("myfile.nbt"));
// Using Path
InputStream in = Files.newInputStream(Path.of("myfile.nbt"));
// Read from file
Map<String, Object> map;
try (TagInput<Object> input = TagInput.of(new DataInputStream(in))) {
map = input.readUnnamed();
}
Network (Only compatible with +1.20.3 clients):
ByteBuf buffer = ...;
// Write to buffer
try (TagOutput<Object> output = TagOutput.of(new DataOutputStream(new ByteBufOutputStream(buffer)))) {
output.writeAny(map);
}
// Read from buffer
Map<String, Object> map;
try (TagInput<Object> input = TagInput.of(new DataInputStream(new ByteBufInputStream(buffer)))) {
map = input.readAny();
}
Network (For older clients):
ByteBuf buffer = ...;
// Write to buffer
try (TagOutput<Object> output = TagOutput.of(new DataOutputStream(new ByteBufOutputStream(buffer)))) {
output.writeUnnamed(map);
}
// Read from buffer
Map<String, Object> map;
try (TagInput<Object> input = TagInput.of(new DataInputStream(new ByteBufInputStream(buffer)))) {
map = input.readUnnamed();
}
"Bedrock" will be referred as "Minecraft Bedrock Edition" where NBT data is present on the following locations:
- Bedrock files, similar to Java, but the bytes are encoded in "reverse" (little-endian) and every file has a header.
- Bedrock network, similar to Java, but the bytes are encoded in "reverse" and numbers are encoded using VarInt32 and VarInt64 (with ZigZag encoding for signed values).
Bedrock files (go below for more information about read/write compressed data):
// Using File
OutputStream out = new FileOutputStream(new File("myfile.nbt"));
// Using Path
OutputStream out = Files.newOutputStream(Path.of("myfile.nbt"));
// Write to file
try (TagOutput<Object> output = TagOutput.of(new ReverseDataOutputStream(out))) {
output.writeBedrockFile(map);
}
// Using File
InputStream in = new FileInputStream(new File("myfile.nbt"));
// Using Path
InputStream in = Files.newInputStream(Path.of("myfile.nbt"));
// Read from file
Map<String, Object> map;
try (TagInput<Object> input = TagInput.of(new ReverseDataInputStream(in))) {
map = input.readBedrockFile();
}
Bedrock network:
ByteBuf buffer = ...;
// Write to buffer
try (TagOutput<Object> output = TagOutput.of(new NetworkDataOutputStream(new ByteBufOutputStream(buffer)))) {
output.writeAny(map);
}
// Read from buffer
Map<String, Object> map;
try (TagInput<Object> input = TagInput.of(new NetworkDataInputStream(new ByteBufInputStream(buffer)))) {
map = input.readAny();
}
Minecraft (sometimes) use multiple types of compression algorithms:
- gzip, for files.
- zlib, for network packets.
- lz4, for files (since 1.20.5).
So you may think: "How I will ever know what algorithm use the data that I'm reading"
For your luck, this library brings an easy-to-use utility class named ZipFormat
to detect and create input/output streams for files and paths with a compression algorithm.
File data = ...;
Path data = ...;
OutputStream data = ...;
InputStream data = ...;
// Create compressed output to write (using gzip for example)
OutputStream out = ZipFormat.gzip().newOutputStream(data);
// Detect compression algorithm and create InputStream to read (using gzip for example)
InputStream in;
if (ZipFormat.gzip().isFormatted(data)) {
in = ZipFormat.gzip().newInputStream(data);
} else {
// Assuming that "data" is an InputStream, otherwise you should create an InputStream for File or Path.
in = data;
}
// zlib offers the option to see what compression level is used (null if is not compressed)
Integer level = ZipFormat.zlib().getCompressionLevel(data);
Important
ZipFormat class offers an instance for lz4 compression algorithm, but the lz4 library must be present on classpath to work correctly.
If you have a DataOuput
instance, you can also create a delegated class with fallback writing for malformed Strings.
DataOutput output = ...;
FallbackDataOutput fallbackOutput = new FallbackDataOutput(output);
You may have seen method names like "writeUnnamed" and "readAny". Why they have names like that? And why there's no a simplified "write" and "read" methods that accept File and Path?
That's because Minecraft have changed across the time (it's a 15th year-old game of course) and the actual encoding options have too many combinations that is practically impossible to support them all with overloaded methods for multiple platforms (Java & Bedrock).
- unnamed, mean a tag that doesn't have a name associated with it (oldest way to encode/decode NBT).
- any, mean a tag, introduced for Java on Minecraft 1.20.2.
- bedrock file, mean a tag written in a bedrock file with its header.
Following Minecraft format, there's no limit to write with a tag output, but tag input is limited by default with a maximum of 2MB size for tag (due network limitations) and 512 stack depth for nested values (since MC 1.20.2).
To change that limits you can also modify the recently created TagInput instance, for example:
TagInput<Object> input = ...;
// For unlimited size
input.unlimited();
// For different size
input.maxQuota(10 * 1024 * 1024); // 10MB
// For different stack depth
input.maxDepth(128);
NBT format can be (nearly) represented on other data formats, this library support them all (as we know).
It's like Json, but most keys are unquoted, and more specific tags has their own String representation, visit Minecraft Wiki for more information.
// Convert to SNBT
String snbt = TagWriter.toString(map);
// Get from SNBT
Map<String, Object> map = TagReader.fromString(snbt);
Note
TagWriter use an AsyncStringWriter to append strings asynchronously, it's like java StringWriter, but it uses a StringBuilder instead of StringBuffer.
It's Json!, some data types may differ between conversion due any NBT array is converted to JsonArray
and json arrays of byte
, int
and long
are converted into byte[]
, int[]
and long[]
instead of NBT List.
// Convert to Json
JsonElement json = TagJson.toJson(map);
// Get from Json
Map<String, Object> map = TagJson.fromJson(json);
"What is this? Why you never heard of this secondary format?"
That's because this format is "new", was implemented on this library if you want to read/write java objects into configuration files (like YAML) without loosing formatting with specific NBT types (like byte and short).
// Convert to simplified configuration
Object config = TagConfig.toConfigValue(map);
// Get from simplified configuration
Map<String, Object> map = TagConfig.fromConfigValue(config);
It's just like /data
command, a simplified way to "colorize" tags with provided/customizable palettes.
// Default palette, using legacy colors
String colored = TagPalette.DEFAULT.color(map, null);
// Json palette, using raw json formatting
String colored = TagPalette.JSON.color(map, null);
// ansi palette, using ansi console colors (if you want to print it)
String colored = TagPalette.ANSI.color(map, null);
// Minimessage palette, using minimessage formatting
String colored = TagPalette.MINI_MESSAGE.color(map, null);
// You can also provide an indent, for example a 2-space indent for multi-line formatted string
String colored = TagPalette.DEFAULT.color(map, " ");
Note
This implementation is NOT "pretty print", it is just a "tag colorization".