Skip to content
MrBlobman edited this page Apr 28, 2016 · 2 revisions

Welcome to the NBTProxy wiki!

For the classes used in this demo see the code page. There will be links directly to the referenced classes whenever they are mentioned or you may find it beneficial to have the code page open in another tab.

Lets imagine we want to store some stats on some pickaxes such as the number of diamond ore broken as well as the amount of damage it has done and a history of previous owners. This tutorial will not be concerned with how to data is obtained just how to transfer it to and from NBT via this plugin.

Creating our first serializer/deserializer

OK so lets begin with creating an NBTSerializer and NBTDeserializer for our PickStats class.

We will call this class PickStatsSerializer and to start us off we will have it implement NBTSerializer and NBTDeserializer.

public class PickStatsSerializer implements NBTSerializer<PickStats>, NBTDeserializer<PickStats> {
    @Override
    public PickStats deserialize(NBTBaseTag nbtBaseTag, SerializationContext serializationContext) {
        return null;
    }

    @Override
    public NBTBaseTag serialize(PickStats pickStats, SerializationContext serializationContext) {
        return null;
    }
}

In order to reconstruct an instance of PickStats we will need to serialize all three of our fields. To make our lives a bit easier and our code easier to maintain we will put all of our tag names in some constants.

    private static final String DIAMONDS_BROKEN_TAG = "diamonds";
    private static final String DAMAGE_DONE_TAG     = "damageDone";
    private static final String OWNER_HISTORY_TAG   = "owners";

Now lets implement our serialize method. It is generally more intuitive to implement serialize before deserialize because deserialize is based on how we serialize the object to begin with!

Implementing the serialize method

First we can create a local reference to the TagFactory for the version we are running.

    TagFactory factory = serializationContext.factory();

Next up on our list is to create a new place to put all of our serialized values. A compound tag is what we are looking for, it is essentially a Map<String, NBTBaseTag>. We use our factory for making new tags.

    NBTCompoundTag serializedStats = factory.newCompoundTag();

Time to serialize the diamondsBroken field and put it in our serializedStats. We will make a new int tag and store our number of diamond blocks broken in there. Then we will put that tag in the map at the key DIAMONDS_BROKEN_TAG.

    NBTBaseTag<Integer> diamondsBroken = factory.newIntTag(pickStats.getDiamondsBroken());
    serializedStats.put(DIAMONDS_BROKEN_TAG, diamondsBroken);

We can do a very similar thing for the damageDone field but using a double tag instead because that field is a double.

    NBTBaseTag<Double> damageDone = factory.newDoubleTag(pickStats.getDamageDone());
    serializedStats.put(DAMAGE_DONE_TAG, damageDone);

For the owners field we can do something a bit different. This field requires serialization of a more complex type. We have an NBTListTag to handle the list part but the contents of the list elements is a little more complicated. So what we will do is delegate it to a different serializer OwnershipEntrySerializer. Check out the SerializationContext#serialize(Object) method.

    NBTListTag ownerHistory = factory.newListTag();
    for (OwnershipEntry entry : pickStats.getOwners()) {
        NBTBaseTag serializedEntry = serializationContext.serialize(entry);
        ownerHistory.add(serializedEntry);
    }
    serializedStats.put(OWNER_HISTORY_TAG, ownerHistory);

Notice this line NBTBaseTag serializedEntry = serializationContext.serialize(entry);. It uses the serializer registered for the type of the object passed into the method. This is a really nice feature that gives us a nice separation of concerns. Our PickStatsSerializer is only concerned with the serialization of things in PickStats and we leave serialization of OwnershipEntry to the OwnershipEntrySerializer.

Lastly we can return our serialized stats.

    return serializedStats;

Implementing the deserialize method

Generally the first step consists of verifying that the passed in tag is an NBTCompoundTag and then safely casting it. This is not always the case as some objects may not need to be serialized as a compound tag but there is most likely always the need to check the type of the tag.

    if (!nbtBaseTag.isCompound())
        throw new NBTException("Base tag is not a compound tag");

    NBTCompoundTag tag = (NBTCompoundTag) nbtBaseTag;

Next up we can really easily grab the data for diamond blocks broken and damage done by calling the get method of the appropriate type and passing in the tag we used when we were serializing the item.

    int diamondsBroken = tag.getInt(DIAMONDS_BROKEN_TAG);

    double damageDone = tag.getDouble(DAMAGE_DONE_TAG);

Lastly we can use the same strategy for deserializng our complex type. We will delegate it to the SerializationContext#deserialize(NBTBaseTag, Class) method.

    NBTListTag ownersTag = tag.getList(OWNER_HISTORY_TAG);
    List<OwnershipEntry> owners = new ArrayList<>();
    for (int i = 0; i < ownersTag.size(); i++) {
        NBTBaseTag rawEntry = ownersTag.get(i);
        OwnershipEntry entry = serializationContext.deserialize(rawEntry, OwnershipEntry.class);
        owners.add(entry);
    }

Finally we can make a new instance of our deserialized type and return it.

    return new PickStats(diamondsBroken, damageDone, owners);

Create an NBTson instance

NBTson is the object you will hold onto for quite some time. This is the class that does the reading a writing at the top level. Create an instance of it with an NBTsonBuilder.

    PickStatsSerializer pickStatsSerializer = new PickStatsSerializer();
    OwnershipEntrySerializer ownershipEntrySerializer = new OwnershipEntrySerializer();
    NBTson nbtson = new NBTson.NBTsonBuilder()
                .register(PickStats.class, pickStatsSerializer, pickStatsSerializer)
                .register(OwnershipEntry.class, ownershipEntrySerializer, ownershipEntrySerializer)
                .build();

Now with your nbtson variable you can very easily read your stats from a pick with some code like the following making use of the read method:

    ItemStack item = player.getItemInHand();
    if (item != null and item.getType() == Material.DIAMOND_PICKAXE) {
        PickStats stats = nbtson.read(item, "stats", PickStats.class);
        player.sendMessage(new String[] {
                "Stats:",
                "    Diamonds Broken: " + pickStats.getDiamondsBroken(),
                "    Damage Done: " + pickStats.getDamageDone(),
                "    Owners: "
        });
        SimpleDateFormat format = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss");
        for (OwnershipEntry entry : pickStats.getOwners()) {
            player.sendMessage("    - " + entry.getOwner() +
                    " from " + format.format(new Date(entry.getStartTime())) +
                    " to " + format.format(new Date(entry.getEndTime()))
            );
        }
    }