I wrote an extensible NBT implementation (XNBT)

So, I started this out of pure boredom but it turned out to eat up much more time than I expected. It’s not the first time I wrote an implementation of Named Binary Tag (this is my second). I don’t like the self-awareness of NBT types in other implementations (and my previous implementation). I think, making types self-aware is an anti-pattern.

I guess, I achieved what I wanted to:

  1. Types are just that. They don’t handle IO themselfes.
  • Tag header and payload are separated from each other.
  • IO is handled by separate classes.
  • It is possible to add custom NBT types.
  • All Minecraft NBT types are included (base types) and the implementation is fully compatible with Minecraft’s data files.
  • A utility class makes handling NBT files easy.
  • The implementation allows to avoid Minecraft’s limitation of having a “root” tag.

A bit more in detail:

  1. In my opinion, types shouldn’t know more than what they’re holding. They shouldn’t care why they’re holding it. It’s not the type’s job to handle (de-)serialization.
  • In XNBT, the payload of a tag is mutable. The header, which contains the type id and name of the tag, is not. All NBT implementations I found so far don’t do this kind of abstraction.
  • A TagPayloadReader constructs the payload of a tag from bytes, a TagPayloadWriter turns it into bytes and a TagBuilder constructs a tag out of its header and payload.
  • It’s possible to write custom tag types based on one of the default (or base) tags (e.g write a YamlTag based on a StringTag) or to implement a completely new tag type by extending from the BaseTag class directly. The latter obviously will break compatibility with Minecraft.
  • Simple as that.
  • The class XNBT has methods to read and write tags from and to files or any other InputStream or OutputStream. This class is also used to register custom types.
  • I don’t understand why they did this in the first place. It’s definitely not necessary to have a root tag. Since the root tag always is a CompoundTag, one might use it to write a model based on its content, but afaik they aren’t even doing that in Minecraft. I therefore saw no reason to not add the possibility to allow for a bit more “flatness” if the user desires to have it this way.

That’s about it. The code and examples for reading and writing NBT files are on github and JavaDocs are hosted here. The documentation isn’t that great at the moment. So, if you feel like adding something to it or the code, knock yourself out and send in a pull request. Or just ask or write a comment here.

Have fun!

@obnoxint
Very cool! Believe it or not, I did a very similar thing, but invented my own format. Bytes are fun to work with, no?

Of course I do believe you and indeed they are. :smile:

Since ListTag implements List and CompoundTag implements Map, these tag types override their interfaces methods. This looks ugly. Also, some methods are not supported and throw an UnsupportedOperationException and I wanted to get them out of my face (and out of my mind).

I also missed a bug which allows to modify the payload of these tags in an illegal way (e.g. adding/putting null or end tags).

Last but not least, I tested the code thoroughly and couldn’t find any problems. NBTExplorer can process files created by the code and it also correctly loads Minecrafts level.dat, and the files contained in the data folder of world saves.

no boolean tag?

The reference implementation (Minecraft) does not specify a BooleanTag. They (Mojang) use StringTags for this purpose and pass the payload to Boolean#parseValue(String), which returns a primitive boolean value.

On another subject: a really cool feature is on its way into XNBT.

So How would I go about modifying say the level_sponge.dat file? I’ve managed to read the tag but not sure how to change it.

I can’t add anything to CompoundTags without throwing UnsupportedOperationException. Even the example in the README doesn’t work.

Aww, man, I didn’t forget to push that change, did I? Well, obviously I did. And I also already made other changes that would break the fix if I would push it now.

Sorry, better wait a few days. Or change line 25 in the file src/main/java/net/obnoxint/xnbt/types/CompoundTag.java to return super.getPayload().put(value.getHeader().getName(), value); yourself.

I got it to work with some work.

LinkedHashMap<String, NBTTag> map = new LinkedHashMap<>();
map.put("enabled", new ByteTag("enabled", (byte) 1));
CompoundTag compoundData = new CompoundTag("Data", map);
LinkedHashMap<String, NBTTag> mapRoot = new LinkedHashMap<>();
mapRoot.put("Data", compoundData );	
CompoundTag compoundRoot = new CompoundTag("", mapRoot);
List<NBTTag> list = new ArrayList<>();		
list.add(compoundRoot);
XNBT.writeToFile(list, dataFile);

You might find it to be more convenient to use the NBTInputStream#readTag and its counterpart NBTOutputStream#writeTag(NBTTag) instead. This way you don’t have to pack your tags in a List<>.

The static methods in the XNBT class are there to get rid of the limitation of having a “root” tag. You don’t need the additional code to deal with a List<> if you already know (in the meaning of reasonably expect) that you basically only have to read or write a single tag and that this tag is a CompoundTag.

File file = new File("nbtfile);
CompoundTag root = null; // needs initialization only because catch block doesn't return
try (NBTInputStream in = new NBTInputStream(new FileInputStream(file))) {
    tag = (CompoundTag) in.readTag();
} catch (IOException e) {
    // Tough luck!
}
root.put(new StringTag("try it", "this way");
try (NBTOutputStream out = new NBTOutputStream(new FileOutputStream(file))) {
    out.writeTag(root);
} catch (IOException e) {
    // More tough luck!
}

You want to use the Gzip-variants for compressed files, of course.

But like, doesn’t the data API deprecate this completely…?

To make this perfectly clear: I won’t stop doing things (for fun) just because other people do things in a different way.

Please create another thread if you like to discuss this further.

Woah okay… I wasn’t trying to be offensive or anything.

Just from a developer’s point of view, the less hard dependencies the better.

It was a more of an implied question of what your implementation accomplishes that both MCNBT and the Data API do not.

Just ask right away.

The answer is: I don’t know; regarding the Sponge implementation, because I didn’t care to look. The two important differences to the implementation in Minecraft are mutable payloads and the separation of data and logic. Oh, and custom tags, of course.

For what MC doesn’t do, is something I’m going to push in a few days or in the middle of the next week. It depends.

You might want to check out MemoryDataView as it’s virtually a map of string to object data (raw data if you will).

Thanks, I bookmarked that for another day. This is like a game to me, and I don’t like to spoil my experience. :wink:

…what…?

If you want a game, take a shot at the Sponge Plugin Competition…

The submissions category feels lonely. It could use more love.