Skip to content

Building NBT Structures

Eric Freed edited this page Aug 25, 2021 · 5 revisions

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.

TagBuilder

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.

Basics

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/List Tags

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.

Begin/End

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.

Contexts

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:

  1. It makes the current scope of the TagBuilder easily distinguishable, as it matches perfectly with the code.
  2. 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
}

BufferedTagWriter

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);
Clone this wiki locally