Yes, what looks like a simple character mod is getting a technical write up. I’m going to be perfectly honest, this is really just a way for me to bitch about the glorious dumpster fire that is the Sega NinjaNext library and ramble briefly about the process I went through to get a proper importer working.
Before we can get into the process that went into reverse engineering and writing code that would go into the Classic Sonic mod’s creation. We need to talk about what exactly the Sega NinjaNext (or NN) library is.
Ninja is the name of an internal Sega library that has existed since at least the Sega Dreamcast days, used in some form in a lot of their games from Sonic Adventure onwards; up to around Phantasy Star Online 2 (a good chart of the lineage for Ninja can be found here). This library has multiple components, from models (Ninja Object) and animations (Ninja Motion) to user interface (Ninja Chao Project).
Now surely formats stretching back that far would be sussed and sorted surely? Well… Ninja has a ton of variations, as the structures of the data within the files tended to change between platforms. To my knowledge (by which I mean an unfinished specification for the library as a whole that was being produced by Radfordhound) there are at least SEVEN unique variations of the NinjaNext file structures, with the structure of the data within often having things added or removed between variations.
Understandably, this would make attempting to reverse engineer and support every variation a monumental challenge. Luckily, for Marathon, we only needed the Xenon version which Sonic ’06 uses.
Initial Reverse Engineering Attempts
Normally, when trying to reverse engineer a file format, I tend to fly completely blind; trying to spot patterns in the data and writing reading code as I go along. For a couple of the Ninja Formats in Sonic ’06, I did actually have something to go off of (this was before I was given Rad’s spec bare in mind).
enemy_data.arc archive, there exists a file called
en_Kyozoress.xto. This is believed to be the Kyozoress model (an enemy summoned in the Mephiles Phase 2 boss), in an interim form between 3DS Max and the actual NinjaNext library itself. In short, this file is essentially a text based version of the XNO (Xenon Ninja Object) format. Using this, I was able to slowly chip away at reading these models, until I hit a snag which would force me to learn about Flags.
A parameter in the Vertex Lists for Xenon Ninja Object is called
STRIDE, which I presumed to be some mistranslation of the word
SIZE but later learnt it’s apparently an actual thing in regards to Vertex Data Storage (can you tell I sometimes don’t realise the significance of things?). This parameter contains the size of each Vertex entry in the list in bytes. At the time, I just blindly read certain bits of data depending on this value, before hitting a roadblock when it turned out that 52 and 76 are not the only sizes the game used, leaving me clueless about how to determine what data needed to be read; as some Vertices skip out on certain data, which thus leads to a lower size value.
Eventually, I took a look at the LibS06 source code (which I really struggle to do considering C++ is something that my brain just refuses to absorb) and stumbled upon the reading code for individual Vertices. After taking a bit of time to understand it, I finally realised that the code was checking for certain values within the
VTX_FORMAT parameter. A bit of adapting into C# later and I had working reading going, but the code for it was a complete and utter mess. But that wasn’t a problem on my radar at the time.
Xenon Ninja Motion, or, Why I Hated Non Fixed Data Types
The next step after being able to read and write Ninja Objects, was to tackle animation stuff. At first I thought this would be simple, as I had a text based version of two different animation types. However, I quickly hit another roadblock which it would take a long time (towards the end of 2021 (this was in February of that year) when I finally came back and completely redid the Ninja stuff in Marathon) for me to solve.
Ninja Motion Keyframe entries (in a similar way to the Vertex Lists above) can be of a variable length, however, the data in the different types can be so different that my workarounds for the Vertices wouldn’t work here. My first thought was to check if the Type called for a Floating Point Number or a Signed 16-Bit Integer and then go from there. But this plan failed once Vector3s were thrown into the mix, as I had no way of knowing if something with a Floating Point Number was a single number or a Vector3. Even with Rad trying to explain it to me, it just went in one ear and out the other (which tends to happen when my brain just decides to not hold onto information). As it turned out, Signed 16-Bit Integers also had versions where there were three values after the Keyframe Index rather than just one, leading to the exact same problem.
Me stubbornly assuming the A16 value was meant to be a Half Floating Point Number(?!) also didn’t help matters… Either way, I stalled on this and threw animation stuff to the side in favour of trying to get a form of importing working.
Rather than writing a Node Table from scratch, XNO Converter uses an XNO as a donor and writes every Vertex as though it is rigged to a bone, even for static models like stage models. As a result, this led to models for custom stages having bones for no reason. Now this wouldn’t SEEM like a problem, and on the Xbox 360 version of Sonic ’06, it isn’t really (besides a performance hit which Greenflower Zone really felt (to the point of crashing the game if MSAA was left on)). But the PlayStation 3 version of the game on the other hand…
The PS3 version of ’06 seems to handle its skinned XNOs a bit differently, resulting in them drawing WILDLY incorrectly. This is not a difference in the actual format, as using proper Xbox 360 stage models in the PS3 version does work fine (but runs like garbage). Most stage models in the PS3 version aren’t even in a Ninja format, instead using another proprietary format which we call SoX Model (based on the Signature in the files themselves), that goes beyond the scope of this write up though.
Another MASSIVE problem with XNO Converter is a complete lack of a batch process system. To convert multiple models to XNOs with it, you had to go one file at a time. For Greenflower Zone, I hit my limit with this, to the point where I wrote a quick hack to help me (including an AutoHotkey script named (rather crassly)
fuck.ahk). One of my hopes with writing my own XNO Converter was to remove the need for this workaround.
What resulted was a tool that never really saw a public release due to its poor quality, under the name of XNO Builder. The code for this tool was incredibly sloppy, with so many “placeholders” (read, things I didn’t know how to do that I said I’d do later but never did) and probably questionable choices. But it worked to produce valid, properly static XNO files. Which I used for the end of Glyphic Canyon’s development, then put to the test from scratch with Seaside Hill’s development. This was about as far as the Ninja stuff went for a while, as it stalled for the longest time, until December.
Ah shit, here I go again…
(Yes that was the commit message for when I got back to working on Ninja stuff)
In December of 2021, I finally decided to have another shot at writing Ninja support into Marathon (as we’d since rebooted the project from scratch for a cleaner codebase). The Object stuff turned out to be mostly smooth sailing, with reference to Rad’s spec, the Kyozoress XTO and my old code. My focus this time was to keep the code manageable, and try and make the files Marathon saved more accurate (which lead to a lot of headaches and a game of whack-a-mole when it came to materials). As a result of this focus leading me to keep different parts of the library separate from one another… Well… Most formats in Marathon can be handled neatly in one file. Ninja? Ninja has…
Fucking NINETEEN files that make up the reading and writing code for the various types (I’ll remind you at this junction that this code ONLY reads and writes the Xenon variant and doesn’t even try to factor in any of the others!).
Either way, I got Object reading and writing working again with a decent degree of accuracy and FINALLY managed a solution for Motion stuff, which was to have four different Keyframe types and use the Sub Motion Type value in the animation to determine which type to use, then handle it with a slightly eyebrow raising chain of If statements.
While the code for it didn’t end up in the main branch, I had begun experimenting with trying to construct a skinned model from scratch on the side. But things didn’t exactly go well with that, with issues such as nodes just not being generated and their placements being… Interesting?
However, after the reading and writing code was merged into the Marathon master branch, I did start to look at something we’d never been able to do. Which was…
This was one of those things that I thought was going to be nice and simple. Load an FBX with an animation in it into Assimp-Net, plug the Keyframe data into an XNM, ???, Profit!
Yeah that didn’t happen. Not at first.
Rotation in animations quickly became an enemy of mine, as I found out that the positional data of my test animation here was importing fine, but rotations were breaking the whole thing. Even if I didn’t animate the model at all.
Eventually (I can’t remember when), I realised that the A16 values that I had assumed where Half Floating Point Numbers were not that at all. They were actually whole numbers, a hunch told me they were stored in the Binary Angle Measurement system, after adjusting my code to store them as Signed 16-Bit Integers then running them through the same conversion I’d previously used for converting the BAMs rotations in Seaside Hill’s SET data, sure enough, the numbers in the animation for Knuckles’ XTM file more or less matched up.
However, this led me down another frustrating trail. That being trying to get Assimp to give me the same numbers as a test animation, to no avail. This whole process could have been avoided, if I’d just tried to plug the numbers it gave me in directly (after converting them to BAMs of course).
Besides a few odd wobbles (which I now assume is caused by frame interpolation), just plugging in the converted numbers worked fine. Meaning my whole process of trying fruitlessly to get the same numbers as the original file was a complete waste of time. None the less, a solution for animation importing was FINALLY a thing. The next big step, would be proper model stuff. Which I just kept putting off thanks to multiple failed attempts and dead ends demotivating me. Until February of 2022.
*insert Europe song here*
There’s not much to say about the actual process of creating a skinned XNO from scratch that I haven’t already written down here. Until we get to the fun that is the Node InvInitMatrix data. For the longest time, this was the final major roadblock, by the time I had everything else working in some form, this was the one that I eventually traced as a problem. By using the Iblis Worm model (
cCrawler.xno), I was able to determine that an issue in my generated node tables was causing the model to break. A bit more data transplanting later, I narrowed down the issue to the InvInitMatrix data, a set of four floating point numbers that have something to do with positional and rotational values in 3D space? I’m going to be honest, this is an area I’m still clueless in; but plugging Assimp’s numbers in just wasn’t working and led to shit like this:
And thus, Skyth comes to the rescue once more (honestly the lad was an utter saint in putting up with the need to help me out for this) by pointing me in the right direction for fixing the Matrix values. Once that was done, I tried the Iblis Worm model, and it was no longer exploding. So I tried Classic Sonic…
And he finally worked…
Well. Minus one catch.
My importing code doesn’t work right on its own. The skinned XNOs it generates crash the game. HOWEVER, they DO work as a DONOR XNO for XNO Converter. Old tools save the day! It was time to finally try and make this cursed mod a proper reality after a year of wanting to do so.
The Classic Sonic Animation Grind
As it turns out, Classic Sonic has a LOT of animations in Sonic Generations, so the first step (I decided) was to convert each and every one of them into ’06 and then work through assigning them.
While this process was a simple one, it was a very tedious one as well. As (rather than do anything sensible), my approach was to open each animation in 3DS Max one at a time (after converting them from a Havok Skeleton Animation through modelfbx), rotate them to the right orientation and scale them up (by a World Units factor of 88) then export them to a new FBX that I passed through Marathon to convert to an XNM.
At the same time as the initial working import was done, GPF graciously offered to provide me a few custom animations in places. Such as the slow walk animation, which Classic lacks in Generations, and the initial animation that made me go “Oh god”, his menu pose.
Over time, I realised a need to merge some animations together. A sensible person would have done this by joining them together in 3DS Max. Me? I did it in code. Because I’m insane.
This code literally just takes all the Keyframes from one XNM and adds them onto the end of another. An absurd way to do it, but it worked. So I kept using it.
From here on, most of the mod was just selecting animations, merging them together where needed and occasionally getting frustrated on an animation looping where it shouldn’t do (before finding out I was making a wrong assumption about how it should work…).
It also turns out that things like the unused Shield and the Yellow Gem’s Thunder Guard Shield attach to a specific node index. I believe this is Node 3 (or something?), normally this would be the Hips bone. But on Classic Sonic (thanks to some modelfbx shenanigans) this was an unused mesh node, resulting in the particles appearing at his 0,0,0 point. So to fix this… What did I do? Did I remove the unused node? Of course not.
I just forced those nodes to take the position of the Hips node in the XNMs instead. A solution that worked, but is another example of a stupid hack (but made a good learning experience for the future at the least).
While the code isn’t quite up to… Well… Code… For a proper release of a model and animation importer, it’s nice to see (and have been responsible for) enough progress in the area to make custom rigged character mods a possibility. Hopefully, at some point, I’ll manage to figure out why my models don’t work on their own (it’ll be nice to finally deprecate XNO Converter) and put together a solution for non skinned models so that XNO Builder can also be thrown out.
As for other games that use different versions of the Ninja library, I’ll leave that up to other people to make breakthroughs on, I’ve suffered with the Xenon library enough and people are still chipping away at other parts of Ninja (XNCP and its descendent SWIF being a big one) to this day.