How I created a training mode for a Playstation Digimon game
Published on the 17/01/2021
Digimon Rumble Arena, originally known as Digimon Tamers - Battle Evolution, is a late PlayStation game that came out between 2001 and 2003, depending on the region where you lived. It is a pretty straightforward platform fighting game with traditional health bar mechanics, simple controls and some more "casual" mechanics, like items, stage hazards and power-ups. The game features 23 characters from the first three seasons of the Digimon anime (up to Tamers), plus one original character created for the game - the arcade boss, Reapermon (or, in Japan, Gokumon).
I played this game a lot, when it came out. I, my brother, and a friend of us spent countless afternoons fighting against each other, banning Seraphimon (yes, we were that scrubby and my brother was too good at using him) and sometimes even organizing some minor tourneys (one of which I shamelessly, lamely won using Patamon's "running the timer" tactics in Reapermon's stage).
Eighteen years later, in my adult age, I have found a way to play this game again online, using the Mednafen emulator and its built-in netplay routine. Thanks to this, I came in touch with a still active player community that holds local tourneys in Japan and New Zealand, showcasing some of the sickest combos I have ever seen done in that game (including some I wasn't even aware were possible).
Out of excitement, I booted up the game again and decided to test something myself, with my old main, Stingmon. I heard about an elusive "infinite loop" involving his standing melee attack, and couldn't wait to try it out. Except, the game had no training mode. Thus, I booted up a 2 players session and moved both characters to a corner, where I started trying out the combo.
Surprisingly, it went down extremely smoothly. The combo counter went up, the health went down, and I was happy to have found a (circumstantial) 0-to-death combo, so I excitedly posted it in the community Discord server. Only to be told that "the in-game combo counter doesn't really work" and that my infinite loop could "actually be blocked after the second hit" but could become a real infinite with a very precise timing. I spare you the swearing.
I booted up the game and headed for 2 Player mode again, with an awkward setup that allowed me to make the dummy block while pressing a button while, with the other hand, I was practicing my timing. Not the best solution, completely impractical, a lot of save states involved. That was the last straw.
I am a physicist, I solve problems. And my problem was practicing a Stingmon infinite in a 18 years old game for a 25 years old console.
My first gut reaction was to do what every sane, grown up, 30 years old man would have done in my place: booting up Visual Studio 2019 to build a C++ program that would send keyboard presses to Mednafen to "simulate" the dummy blocking, without me having to do anything on my joypad. While the idea was sound and seemed to work properly in theory (opening Notepad to test and seeing the keys being sent with the rhythm I wanted), in practice mednafen would completely disregard my program and laugh at me for my puny attempt at steering it.
It wasn't a simple accident. It was a declaration of war.
On BizHawk, LUA, and why you shouldn't challenge a physicist
I was extremely salty. One afternoon wasted because a pretentious emulator wouldn't accept my simulated inputs. That evening, I asked around in the Digimon Rumble Arena community server if anyone had any pointer... and that's where BizHawk and I crossed paths for the first time, in person. I had already heard of BizHawk, thanks to an interesting article about a group of business analysts, working for a company in the field of machine learning applied to finance, using it to teach their AI to play Street Fighter II.
BizHawk is not only an emulator: is a full suite of tools for tool assisted speedruns, with routines to inspect and edit the RAM of the emulated machine, and a powerful Lua scripting environment. When I first started delving into it, I realized that that was the right weapon for my battle.
I didn't know anything about LUA, but I am a pretty proficient programmer myself, someone who grew up on Ruby (hi, RPG Maker XP), translated his knowledge to C++, built a C# GUI application for a physics collaboration without almost knowing the language at first, and being the one guy who had to maintain a huge LabView code base for his experiment (if you don't know what LabView is, think the Fisher-Price of programming languages, with square drag&drop blocks that represent actions and wires connecting them for building the data flow, without practical tools to zoom in or out the canvas. If it sounds like an unmaintainable nightmare, it's because it is).
What I needed was just a run-of-the-mill LUA basic tutorial, and I was ready to go. The first tests went surprisingly smooth. I could send joypad inputs via the LUA script and control the second player via code, making it block, sweep, guard and attack on a whim. Building a basic GUI was also quite straight-forward, thanks to BizHawk's Lua libraries. In less than an afternoon of work, I had a very rough enemy controller script running.
Then, I moved to step number two: freezing life bars. There is no way one can have a proper training mode if the timer keeps ticking and you risk to KO the opponent after the 15th failed attempt at getting that Stingmon infinite. Sure, save states help, but once I started doing something, I wanted to do it as good as possible.
To this effect, I was finally ready to dive into the magic world of searching the RAM. And - oh, boi - I was going to get a run for my money.
Lifebars, a red herring, and shady GameShark cheat code websites
The first thing that comes to mind, when finding yourself staring at an endless table, containing some 500'000 odd unordered numeric hexadecimal values, is questioning your life choices.
Because, if you ended up reverse engineering the raw memory addresses of a game so old it would be legally allowed to drink and vote, something had to have gone wrong, somewhere in your past. Then, you get back to your senses and remember why you are there, staring at those numbers like Neo would at the source code of the Matrix: you are here to lab that Stingmon infinite.
It turns out, RAM address reverse engineering, at least for fighting games, is not as painful as it looks at first, as you can quickly make out many of the values you need. You are looking for the timer? Check for a value which keeps going down every frame, all things being equal. You are looking for a health value? Perform a search after every hit and compare the values in RAM with their previous one, until you find the one which keeps going down only after you hit the opponent.
There is a certain logic and beauty in all of this.
Thus, with the genuine naivete of a starry-eyed beginner, I directed all my efforts at finding those values. I selected Gatomon and Agumon, chose one of the least annoying stages (trust me, you don't want to do any of this at Recycling or Revolution), and booted my memory watcher. In a couple minutes, I could find two promising values, going down from 120 to 0, in tandem with the characters' health bars. I thought I hit a sweet spot, saved those addresses and changed the stage to confirm my findings. The values didn't move. Boom, Jackpot! Then, I performed the last test: changing characters. And that's when my world fell, broken in a dusty shower of debris.
What I expected to find: a value between 0 and 120.
What I really found: random gibberish that could probably summon Cthulhu or another Great Old One if read inverted, in front of a mirror, for three times in a row.
Those memory addresses were character dependent. And, cherry on the top, they weren't even the real health values - just the current size in pixels of the HUD lifebars. Swearing ensued. Heavy swearing. I was back to square one. I didn't find any rule about the placement of those numbers and didn't have any ideas at that time. Thus, I used my secret weapon: websites for GameShark cheat codes.
Surely, someone must have figured out how to have infinite HP for a character. It's cheat codes 101. Infinite score, infinite timer, infinite life and 1 hit KO. You can expect people to have found out any combination of the above, for every respectable early console fighting game. And I was lucky. On a rather shady, early '00s looking website, I found which memory addresses to poke at to freeze life bars and the timer at will. It wasn't pretty, it wasn't perfect, it crashed the game if this code was injected during a loading screen, but it worked.
Quick, dirty, and degenerate, but it did its job wonderfully: now I had a very basic LUA training mode script loaded inside Digimon Rumble Arena to control the opponent and freeze health values.
As I completed the script for the basic moves, I decided to add a second function to force the dummy to perform a specific movement, be it walk, dash, jump, or other amenities. Everything went okay, at first, except for one, trivial aspect: I couldn't make the dummy move towards or away from the player, only absolutely left or right. I didn't have access to the position values, thus it was to be expected. But it was annoying. Very annoying. Worse, it was borderline useless, in its current state, with the opponent character going steadily, stubbornly in one given direction, with utter disregard for the player. I had found my next problem to solve, and for the love of me, I wouldn't have left any stone unturned.
Hope spots, position addresses, and the light at the end of the tunnel
After a good night of sleep, I was ready to dive again into the memory. I had to find those X position values. That was the first step for a functional movement option - I couldn't settle for anything less. The process of finding the position wasn't too hard, at first. I selected two characters, selected the Volcano stage, and poked around the memory, until I found a signed 4 bytes integer which moved in synchro with player 1, plus another similar value that moved with player 2.
Remembering the previous red herring, I moved MUCH more carefully, switched stage and the second character, repeated the experiment. Same values. Then, switched the first character and checked if their position was still where it was supposed to be. All checked out.
I let out a smile. Something going as planned for once. I edited my script, switched the horrid right/left movement for the much more elegant towards/away and called it a day. That's when I tried recording a clip of it to showcase the function in the Digimon Rumble Arena Discord server. And that's when everything seemed to fall - again - apart.
Go back one paragraph, read it again. I switched stages several times, and I switched the second character. Then I switched the first. But I didn't check the address of the second character position again, when I performed the last test. That, that was my mistake.
It turned out that, while the position memory address of the first character was completely independent of the stage and the character themselves, that of the second character wasn't. Swearing ensued, together with a healthy dose of bashing my head against the wall, a couple times. The bad news: I wasn't done yet. The good news: I had a trail.
The address of the position of the second character depended only on who the first character was. And the address of the first character was instead always fixed. It wasn't as bad as I thought. There was still hope.
Since I found out this value was stage-independent, I simply picked all 24 characters once, against one single, randomly selected opponent, at Volcano. I found out the starting value of the X position of the second player (327680, a number that I will hardly forget after this experience), tuned my memory search on it and, slowly but surely, filled a hash table that connected the player 1 character index to the memory address of the position of the player 2 character.
The memory address where the character indexes were stored were again retrieved on the above, shady website, since "playing as any character without grinding arcade to unlock them" is another premium member of the "fundamental cheats for any '90s fighting game"TM club.
With this knowledge and a quick rework, the script was finally doing what it was supposed to: moving the dummy towards or away from the player while performing a selected action. I called that a win.
I had dived deep into the Matrix and came out with the treasure I was looking for. That would have been enough for most people, but I felt I could do more. I had the feeling I had cracked something, and that I was finally nearer to bite that forbidden fruit of knowledge.
It was time to raze hell. With the big guns.
Which WarGreymon is the strongest
Eighteen years ago, my brother and I couldn't agree on which WarGreymon (Traditional or Black), had the strongest super move. Terra Force and Terra Destroyer. Yin and Yang. No training mode with numeric values in Digimon Rumble Arena, remember? And measuring life bar sizes with a ruler on the curved screen of a 4:3 cathodic tube television set wasn't really practical (you are welcome to try it at home and send me the handwritten result via homing pigeon - I'll wait). Thus, the infinite discussion on which WarGreymon was better. It was time to crack the code. It was time to find this out, for real, backed by numbers and science.
The success with position values galvanized me. I felt confident I could crack the code, thus looked again for those 0-120 values that eluded me at the first attempt. They were only dependent on both the selected characters, but completely independent on the stage. There was no fixed address, but there HAD to be something, a rule to sort them out.
The first attempt was in vain: I tried to use the same offsets I retrieved for the positions, but it didn't work at all. And that's where maths came into play. I started making experiments with three characters - Gatomon, Agumon, and Guilmon. I ran the matches in all possible combinations and noted down the relevant memory values.
I suspected that, since the positions didn't depend on the stage, the address of the first player had to be stored at a specific point a + x + y in memory, where a was a fixed offset containing non-character related variables, x was the size of the first character's variables and y the size of the second character's variables. It was a hypothesis I had to verify. Scientific method, you see? a had to be constant, otherwise my whole construction couldn't have worked.
It was my leading hypothesis. I assigned x to Gatomon, y to Guilmon and z to Agumon, thus building a linear system with three equations and four variables.
Linear systems in four variables ?!
If you have even a basic understanding of math, this should ring an alarm bell as loud as the Geiger counters during the Chernobyl disaster: That linear system was undetermined by definition.
I needed a fourth equation, and that's where I got the idea of exploiting mirror matches. In that case, the memory address - if my hypothesis held any regard, had to be equal to 2x + a. I eagerly set up my newly completed linear system and fed it to an online solver (don't judge me, solving linear systems with six figures numbers on Sunday afternoon is not my cup of tea, okay?). I inputted the equations, pressed the solve button, and waited. Only to find out that one of the equations was linearly dependent on the other 3.
First reaction: how the heck can this be? Now, which other fourth equation can I find to make this system solvable?! I need to find the value of a!
Eureka moment just a couple minutes later: if one of the equations is really dependent on the other three, this means that my theory is sound!
Yes, I didn't realize it at first, but this was proof that my conjecture held: if the fourth equation could be built by using the other three, that meant that the system had infinite solutions, no matter which value I chose for the offset. Normally, if you have a system with one redundant equation, the system is either impossible or has infinite solutions.
This one fell in the second category, and literally meant that I couldn't care less about the value of a, thus I just set it to 0. This effectively moved half the value of a, whatever it was, inside the other variables, making the system solvable for the new variables. Moreover, I didn't need a system of equations anymore: a single mirror match could yield the correct value for each character, because the equation would simply become 2x = address. Thus, I only needed to get the block size for a character once to be able to cover any match up involving that character.
To test if my theory was really sound, I booted up one mirror match with Renamon, calculated the value, then summed it to the value I retrieved for Gatomon, crossed my fingers and noted down the resulting memory address. Then, I started a Renamon vs Gatomon match, and watched the previously calculated value at the start of the match.
I cannot vouch for my neighbors, but due to how thin the walls of my flats are, I am pretty sure they heard my unholy scream of jubilation, when the value I peeked was actually 120. Boom, headshot! I frantically went back to the memory watcher and found out that the value for player 2 was always 132 bytes far from the address of the first player. The values of the Digi bar (the super meter of the game) followed a similar pattern.
Now, I just needed to play 19 mirror matches to retrieve the missing values. I started with BlackWarGreymon. Then, Wormon, the character with the lowest defence score in the game, according to the community. Then, WarGreymon.
I set up my test script, showed the life bars as percentage numbers, charged my super with both WarGreymons and performed it on a happy-go-lucky, blissfully unaware Wormon.
Terra Force dealt a devastating 52% damage burst that made the screen tremble. But Terra Destroyer turned it up to 11, by depleting 68-freaking-% of the poor worm Digimon's life bar. Sixty-eight percent. In one go. I let out a Wooo, Ric Flair style. That was it. That was my pure, unadulterated victory.
It was late Sunday night. I finally had a working script for showing the health totals of each character and printing it on the HUD. And, after 18 years, I finally settled the argument with my brother.
The last obstacle, or why you should care about canaries
In the moment of excitement, I realized that, in tackling all those very interesting issues, I neglected the problem I was bent to solve at the very beginning: practicing that Stingmon infinite.
I could make the opponent block, sure, but not after the first hit - which was, admittedly, exactly the one thing I needed to practice an infinite combo. I thought about it for a while. I didn't know how to datamine the hitstun values. I didn't really want to dive back into that digital hell. Thus, I did what any physicist would have done in my place: found the lazy solution and looked for a canary.
Okay, before you start calling the police to get me locked into a psychiatric hospital, I am not talking about a tiny, yellow-feathered bird. I had three canaries in my life, Titti, Ozio and Kimi, but this isn't about them. You know the saying "a canary in a coal mine" as a marker for signs of imminent danger? That was the canary I was talking about: an unrelated object that changed its state only when or just as the other character was hit.
I thought about two or three possibilities, the first one being "the life bar size", the second being "the Digi bar size". Possibility #1 had a huge drawback: when the lifebars were frozen by the script, it couldn't possibly work. Possibility #2 had a similar, yet unrelated, drawback: the Digi bar has a maximum cap, above which it doesn't increase anymore. This made me think about possibility #3: the score.
You see, in this game every move has an associated score to it, which is awarded if it connects with the opponent and it's not blocked. The score of the attacking player increases as their moves deal damage to the opponent. Coincidentally, the addresses of the scores were among the few addresses that were completely character-independent, and I had already found them by chance during one of my previous dives.
I added an option to the script to trigger the desired action when the score of player 1 increased, crossed my fingers, and loaded it into the game.
As the dummy blocked the third hit of my supposed Stingmon infinite, my heart raced with joy and profound sadness.
Joy, because I had finally solved the problem that set everything in motion and built a fully functional training mode for an old emulated PlayStation game.
Profound sadness because, as it turned out, I was far from being able to loop that jab more than twice in a row, with my current training.
Whatever, if I managed to do all of this in three days, I can also manage to get my timing right and perform that Stingmon 0-to-death loop correctly at least once, can't I?
Thanks to Andrea for his sense of humour and his willingness. The code for his training mode script is available on GitHUB. Formatting by Neithan and Pr0sk.