One of my favorite games of all time is Super Monkey Ball for the GameCube. I’ve played hundreds of hours of this game, most of that competing for high scores with a good friend. It’s an incredibly fun dexterity challenge game, but my friend and I have one major complaint: the scoring system totally sucks.
The two main problems with the scoring system are the point value of the collectible bananas, especially the bunches, is set way to high; and the level skip bonus is way too big. If you’re at the point where you’re competing for high scores, this distorts the gameplay. Your goal ends up being trying to collect as many banana bunches as possible, and to take every level skip. That means you’re incentivized to play less of the game in order to get those level skip bonuses; and if you’re good enough to complete the game with a few extra lives, you’re actually encouraged to throw your monkey off the edge in order to collect a few extra banana bunches to top that old high score. There are some subtler effects as well, for example a very small level completion time difference can result in a massive score difference when it’s multiplied by the level skip bonus. We want to encourage beating all of the levels quickly, with a small bonus for grabbing bananas.
So I decided to try hacking the game in order to lower the banana bunch score value, and eliminate the level skip bonus. While we have some other ideas for how to improve the game, I figured these two changes would be pretty easy to accomplish for a novice game hacker like myself. I’ve documented my process here in this blog, in the hopes that it’s useful to someone else who wants to hack a GameCube game using Dolphin, and for myself when I want to dig back into this project and remind myself how the tooling works.
Most of this was done with the Dolphin emulator’s outstanding debugging tools, which you’ll see in screenshots below. If you want to follow along, I’m using the US release of the game. The ISO that I’m using is 1459978240 bytes long and MD5 hashes to 391b8e620d8d924f150dc40343ced8a5.
Let’s get started by attacking the banana bunch value.
First step to finding the code that modifies the score when you collect a banana is to find where the player’s current score is located in RAM. So I booted up the game, entered the first level, collected one banana, and then made a save state just before collecting another banana. The Cheats Manager in Dolphin lets you filter all RAM for locations containing a specific value. I guess that the score is probably stored in a 32-bit integer, so I filtered for 32-bit integer values containing 100. That found about three hundred results.
Then I resumed the game and collected the other banana, then filtered again for values containing 200. Only two results remained.
I jotted down those two memory addresses in my notes, then used the Dolphin memory editor to change the value in the first found location to something else.
When we start the game again, this will show us whether this location is actually the player’s score value.
Boom. Found the score location. (Some later experimentation showed that the other location is used to track the “target score” as the score climbs when it is changed. Not interesting to us.) The next task is to find the game code that modified this location when I collected the banana. So I added a memory write watchpoint at this memory address and loaded that earlier save state.
Back in the game, I scooted over to collect that banana again, and the emulator broke on a stw
instruction, which is modifying the player’s score location. Looking good!
I jotted down the location of that instruction, as well as the value of r5
.
Looking at that stw
instruction, I would guess that r5
contains the address of the current player’s data structure, and that each player’s score is stored at offset 0x7c
within that struct. Also notice that the value in r4
is 0x64
, or 100. That’s the score value of a single banana, and you can see just one instruction before the stw
is an add
, which adds that value to the register that is then stored in the player’s score location.
So in order to find the value of a single banana, what we need to find next is where the value in r4
comes from.
Browsing through the nearby code, I didn’t see anything in the immediate vicinity that loads a value into r4
. I made a guess that perhaps this is some kind of “add value of r4
to current player score” routine, so I moved one call up the callstack (note the LR
address in the callstack viewer). Here’s what I found there.
Well hey, it’s a load into r4
. Looks like my guess might be right. So I set an execution breakpoint at that lha
instruction to see what the contents of r4
are before and after that instruction.
It loaded 100, so that looks like it is our banana score lookup. So let’s go look at what’s going on at the location that it loaded from, which is 0x801bdea0 + 0x4c
, or 0x801bdeec
.
You can see our individual banana score value right there at the expected location. So that’s what we need to change if we want to change the value of individual bananas. But we’re after bigger game: banana bunches. Very likely the value is somewhere nearby in this table of data, but I’ll do it the easy way and just go back to the game and grab a banana bunch, and see what the value of r4
is when we hit the breakpoint we set earlier.
There it is. Go look up offset 0x4c from that address and we have the address of our bunch value.
So we need to change the 2 bytes at 0x801bdf00
from 0x03e8
to whatever we want the bunch value to be. We can tune the right balance later, but for now let’s set it to 250 and verify the theory.
Check it out, the score changed by only 250 points after I collected the bunch (and then fell off the edge of the stage). Perfect!
The next part is a little clumsy. In order for this change to be permanent, instead of just a temporary hack in this Dolphin session, we need to change this value in the ISO file on disk. But I don’t know how that runtime memory location is mapped to the disc image. What I ended up doing was just dumping the image with xxd
(5.8GB text file!), and then editing that hex dump in vim
. I searched for a unique sequence of nearby bytes that I saw in the memory viewer, and found the banana bunch score value that way. There’s definitely a better way to do this.
Anyway, there’s our banana bunch value in the ISO, at location 0x1d2f00
. I edited it to 250, converted it back into a binary ISO, and tested it in game and… it worked! Every banana bunch is now worth only 250 points. Awesome!
So that’s the easier half done. In part two, I’ll describe how I found and eliminated the level skip bonus. It involves loading the game into Ghidra in order to decompile GameCube code, which turned out to be really cool.