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:
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:
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.
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.
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.
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.
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.
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.