So the majority of my week was working on writing the module system for the RenderEngine, since most of the core functionality relies on it. And with that, I looked up some of my old code to use for it, and oh boy. First, a little bit of explanation on the things I did, and then the trip down memory lane.
So, modules. I looked up a few projects that I knew of that had a loading system that I knew of to get an idea on how to implement it. Bukkit was my first go to, but I forgot that was taken down by DMCA because of shenanigans. Forge, as I remembered back in the day, was full of black magic and I honestly didn't want to burn out my brain trying to unwind how it worked. I looked over a few random repos on github when I remembered that Spigot was a thing. Spigot is basically what Bukkit used to be, but without the DMCA. And, most importantly, it has a fairly simple to understand plugin loading system that I can study.
Spigot's plugins use a metadata file to describe itself and point to the class extends the class that Spigot needs to use to load and control the plugin, using YAML, which depending on who you ask is Yet Another Markup Language or YAML ain't markup language. It's such a simple, yet genius, way to do it. And it works well for Spigot because it uses YAML for a lot of what it does, mostly for permissions to things. For us, as humans, YAML is great to make easy to read and edit data files; For us, as programmers, snakeyaml (the parser most commonly used for parsing YAML files in Java) is a pain in the ass to use unless you have a spec that you want concrete and won't change. Which I don't have, since I'm not sure what I need to do with my modules or how they need to fully act with everything yet. So I wanted to find something that was easy enough to modify during development without also needing to rewrite the parsing logic a metric shit-ton of times.
Firstly I looked at XML, but it's bloaty. It's super human friendly, but like YAML, it's not very programmer friendly. Same with JSON (JavaScript Object Notation). And I was dreading writing something custom and one-off just for this. I just about made my decision to stick with YAML when I had a thought: Why don't I use NBT?
Now, NBT is short for Named Binary Tag. It's the data format use for various Minecraft files. It's not super human friendly because it's written as bytes to the file, but there are enough third party open source apps that can parse and make these files human readable and editable that it's a moot point. It's also super programmer friendly, because all you really need to do is make a Compound, add other tags to it, and stuff it into a file and it's done. Literally
NBTCompound compound = new NBTCompound("root");compound.add(new NBTInt("health", 100));compound.add(new NBTString("name", "Billy Bob Joe"));NBTFileOutputStream.write("file.nbt", compound);
and you had an NBT file. AND back in 2015 I wrote my own parser library for it following the Minecraft spec. I though it was the shit. I was so proud of myself for it and I posted it on my Github as part of my libHACK library.
Oh boy, now in 2023, I never knew how wrong I was.
I started looking over the code and I started having those moments of "da fuck was I thinking?" over way to much of it. There were so many bugs in just the way the NBT tags deal with parenting that I'm surprised that it worked. For instance:
- The parent tag would get a new child tag added to it. Sounds normal so far.
- The parent tag would then check to see if it had a tag with the same name as the new child tag in it's underlying Map, get it, AND REMOVE ITSELF AS IT'S PARENT AND CAST IT TO THE SIDE. We now have an orphaned tag because it had the same name as a new tag. (Bug)
- The parent tag then checks to see if the new child tag has a parent, and if so, check to see if that parent is itself. If it does have a parent that isn't itself, it removes that parent reference from the child (Pretty sure there is a bug in this logic as well in some weird edge cases)
- The parent tag then sets itself as the parent of the child tag.
So far, we have at least one bug, maybe two, but it doesn't end there. Because of my brilliance all of these APIs were public, due to them being in an Interface. Interfaces in Java can't have protected (either package or class) or private methods, and because I was all into putting everything into an interface because Fuck Yeah Interfaces! at that point of time, I had to have most of the same logic in the abstract implementation of the NBT tags, since it could be called anywhere in the code that had access to NBT tags. And since NBT Containers also implement the Abstract NBT implementation, there are some logic bugs where parent's can recursively become parent's of themselves, stuck in a loop of removing and adding itself. Yeah...
Needless to say, I worked on making my old code not suck by making proper interfaces and using package protected and private methods to reduce the amount of stuff that's seen in other parts of the code where it's not needed. I'm also starting early and making JUnit tests for all of this because I don't want to be 9 months into development and realize there's a logic bug in this code that causes all kinds of fun things that are hard to chase down. Like I probably should have done in 2015, but I was dumb back then.
I also made the decision to not follow the Minecraft spec for NBT, and instead use my own. So those third party apps wouldn't work out of the box for my files, but I should be able to find one of them that's easy enough to hack to work for mine that it's not a huge deal for me. But having my own spec allows me to do a lot of things that the Minecraft spec doesn't easily allow for, like nesting Compounds inside of other Compounds (since the Minecraft spec doesn't have a way to signal the end of a Tag, or the end of the file). I'm not entirely sure if this is still the case now, but it was back in 2015 so that's what I'm going on.
Once I finish all of the Tag classes I need, and the JUnit tests, I'm going to work on the module loader and hopefully get a bare bones test working by the end of the month. That seems like a good goal to shoot for. Right? Right.