-
Notifications
You must be signed in to change notification settings - Fork 9
Building NBT Structures
While SharpNBT can handle all serialization/deserialization for you automatically, it can't fully take away the tediousness of building a complete NBT document from scratch, though it does include tools to facilitate this process to make is as smooth as possible.
The TagBuilder
class provides a methods for building complete NBT structures from nothing, using nothing but POD (plain old data) data types.
Simply create a TagBuilder
instance like any other object, passing in the name of the top-level Compound Tag that represents the document.
var builder = new TagBuilder("My NBT Document");
The TagBuilder
class has a plethora of methods for adding data to it without the need for creating intermediate tags.
builder.AddInt("Health", 9000);
builder.AddString("PlayerName", "Herobrine");
CompoundTag result = builder.Create();
This would give a structure that resembles the following:
TAG_Compound("My NBT Document"): [2 entries]
{
TAG_Int("Health"): 9000
TAG_String("PlayerName"): "Herobrine"
}
One benefit of the TagBuilder
class is that every method returns the TagBuilder
instance itself, so calls can be easily chained similar to that of aa LINQ query. This is the equivalent to the above example:
var tag = new TagBuilder("My NBT Document").AddInt("Health", 9000).AddString("PlayerName", "Herobrine").Create();
Compound and List tags are what define the document structure, as they are the only types that can have contain child tags as their payload. Given that they must be "opened" and "closed", they have some special handling. For that, there are two options.
To create new "nodes", use the BeginCompound
and BeginList
methods. Using these will create a new "nested" scope within the structure which will persist until a call to EndCompound
or EndList
respectively is called.
Click to expand example using begin/end methods
var builder = new TagBuilder("Level")
.BeginCompound("nested compound test")
.BeginCompound("egg").AddString("name", "Eggbert").AddFloat("value", 0.5f).EndCompound()
.BeginCompound("ham").AddString("name", "Hampus").AddFloat("value", 0.75f).EndCompound()
.EndCompound()
.AddInt("iniTest", 2147483647)
.AddByte("byteTest", 127)
.AddString("stringTest", "HELLO WORLD THIS IS A TEST STRING \xc5\xc4\xd6!")
.BeginList(TagType.Long, "listTest (long)")
.AddLong(11).AddLong(12).AddLong(13).AddLong(14).AddLong(15)
.EndList()
.AddDouble("doubleTest", 0.49312871321823148)
.AddFloat("floatTest", 0.49823147058486938f)
.AddLong("longTest", 9223372036854775807L)
.BeginList(TagType.Compound, "listTest (compound)")
.BeginCompound().AddLong("created-on", 1264099775885L).AddString("name", "Compound tag #0").EndCompound()
.BeginCompound().AddLong("created-on", 1264099775885L).AddString("name", "Compound tag #1").EndCompound()
.EndList()
.AddByteArray("byteArrayTest (the first 1000 values of (n*n*255+n*7)%100, starting with n=0 (0, 62, 34, 16, 8, ...))", GetByteArray())
.AddShort("shortTest", 32767);
While it definitely is not the prettiest way to accomplish the task, and it could be become cumbersome to debug in a large document, it does allow for building documents in a compact manner. This method is more suited for small amounts of data where the structure is nothing too complicated and does not contain deeply nested tags.
The second (recommended) way to accomplish this is through the use of the NewCompound
and NewList
methods. Instead of needing to be paired with a closing EndCompound
/EndList
method, they return a Context
object that implements the IDisposable
interface. The actual object is very lightweight and rather unremarkable, and do not actually contain any unmanaged resources, but they do allow for it to be used in a using
block. This has two distinct advantages:
- It makes the current scope of the
TagBuilder
easily distinguishable, as it matches perfectly with the code. - The Compound/List tag for the context is automatically closed when the block exits.
Click to expand example using Context objects
var tb = new TagBuilder("Level");
using (tb.NewCompound("nested compound test"))
{
using (tb.NewCompound("egg"))
{
tb.AddString("name", "Eggbert");
tb.AddFloat("value", 0.5f);
}
using (tb.NewCompound("ham"))
{
tb.AddString("name", "Hampus");
tb.AddFloat("value", 0.75f);
}
}
tb.AddInt("iniTest", 2147483647);
tb.AddByte("byteTest", 127);
tb.AddString("stringTest", "HELLO WORLD THIS IS A TEST STRING \xc5\xc4\xd6!");
using (tb.NewList(TagType.Long, "listTest (long"))
{
tb.AddLong(11);
tb.AddLong(12);
tb.AddLong(13);
tb.AddLong(14);
tb.AddLong(15);
}
tb.AddDouble("doubleTest", 0.49312871321823148);
tb.AddFloat("floatTest", 0.49823147058486938f);
tb.AddLong("longTest", 9223372036854775807L);
using (tb.NewList(TagType.Compound, "listTest (compound)"))
{
using (tb.NewCompound(null))
{
tb.AddLong("created-on", 1264099775885L);
tb.AddString("name", "Compound tag #0");
}
using (tb.NewCompound(null))
{
tb.AddLong("created-on", 1264099775885L);
tb.AddString("name", "Compound tag #1");
}
}
tb.AddByteArray("byteArrayTest (the first 1000 values of (n*n*255+n*7)%100, starting with n=0 (0, 62, 34, 16, 8, ...))", GetByteArray());
tb.AddShort("shortTest", 32767);
As you can see, it is far more expressive and easier to follow. You won't be fighting your IDE trying to "fix" your indenting, and it will in fact be assisting you visualizing the working scope of each section of the document.
Whichever way you choose, the above two examples both result in identical output, which can be used as a reference for comparison.
Click to expand output
TAG_Compound("Level"): [11 entries]
{
TAG_Compound("nested compound test"): [2 entries]
{
TAG_Compound("egg"): [2 entries]
{
TAG_String("name"): "Eggbert"
TAG_Float("value"): 0.5
}
TAG_Compound("ham"): [2 entries]
{
TAG_String("name"): "Hampus"
TAG_Float("value"): 0.75
}
}
TAG_Int("iniTest"): 2147483647
TAG_Byte("byteTest"): 127
TAG_String("stringTest"): "HELLO WORLD THIS IS A TEST STRING ÅÄÖ!"
TAG_List("listTest (long)"): [5 entries]
{
TAG_Long(None): 11
TAG_Long(None): 12
TAG_Long(None): 13
TAG_Long(None): 14
TAG_Long(None): 15
}
TAG_Double("doubleTest"): 0.4931287132182315
TAG_Float("floatTest"): 0.49823147
TAG_Long("longTest"): 9223372036854775807
TAG_List("listTest (compound)"): [2 entries]
{
TAG_Compound(None): [2 entries]
{
TAG_Long("created-on"): 1264099775885
TAG_String("name"): "Compound tag #0"
}
TAG_Compound(None): [2 entries]
{
TAG_Long("created-on"): 1264099775885
TAG_String("name"): "Compound tag #1"
}
}
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 elements]
TAG_Short("shortTest"): 32767
}
The BufferedTagWriter
class was included to assist in the use of SharpNBT for implementing a network protocol that uses the NBT format. It is inherits from the standard TagWriter
class, but differs in that it does not accept a Stream
object for initialization, and keeps its own internal buffer.
The purpose behind this is that network protocols typically require the length of the complete packet prefixed at the beginning, which can not be achieved if using a standard TagWriter
to write directly to the stream. Mix in variable-length integers and different compression formats that NBT supports, and it creates the headache of constantly building your own temporary buffers, writing to them, then calculating the length.
The BufferedTagWriter
combines these steps into one. You merely specify the protocol and compression to create the writer, and can then query it to determine the final size of the payload, accounting for compression and other variable factors. It can then be used to write its payload directly to the network stream when needed, or you can copy it like any other byte array.
using var bufferedWriter = BufferedTagWriter.Create(CompressionType.GZip, FormatOptions.Java);
// Write data with it like any other TagWriter
long length = bufferedWriter.Length; // Size of internal buffer
byte[] bytes = bufferWriter.ToArray(); // The payload
await bufferedWriter.CopyToAsync(myNetworkStream);