Development
Disclaimer
Ok, technically what I've done here is not development but reverse engineering, but that seemed too long to use as a title for this page. Also, I am not in any way an RE guru, I'm a software developer just like everybody else, but after learning about the "SNESticle" string on the Fight Night Round 2 ISO, I couldn't stop thinking about it and eventually decided that something had to be done about it.
The purpose of this page is to document what I have done (including some dead ends that didn't make it into the final release), for myself, and for others who might be interested in the more technical aspects of this hack and SNESticle itself. It's definitely not a tutorial on GC hacking, but if you happen to learn something, fine by me! And if you feel that I went about something in an exceptionally stupid way, please let me know because I would love to get better at this.
In this document I will be referring to Fight Night Round 2 as just Fight Night, and Super Punch-Out!! as just Super Punch-Out.
Tools
For disassembling (and decompiling) I used Ghidra, an open source RE tool by the NSA, and let me just put it out there right away: the NSA is not my favourite organisation in the world, but Ghidra is an impressive piece of software. I also think there's something inherently funny about RE-ing old video games using software provided by a morally corrupt government agency.
I only really used Ghidra to browse and comment the code a bit. For testing patches I used hexcurse. It's a perfectly reasonable hex editor with a really dull UI. Why aren't there any cool terminal-based hex editors?
Dolphin was of course invaluable for testing and debugging. It's really quite excellent.
VBinDiff came in handy for comparing binary files.
For writing PPC code I used disasm.pro, a rather awesome online tool that can assemble as well as disassemble PPC code with absolutely no context or boilerplate needed.
For real hardware testing I used Datel's SD Media Loader and Swiss.
First step towards wars
The first thing I wanted to do was to see if I could find the Super Punch-Out
ROM on the Fight Night ISO and then try to replace it with something else. My
main worry was that it might be encrypted, or there could be checksum checks in
place to prevent tampering. Thankfully there was nothing of the sorts and
Dolphin made this first step really simple. It is able to extract all of the DVD
contents into a directory, and can even run the game directly from the extracted
files, making for super quick patch+test cycles. Conveniently enough, Super
Punch-Out is located right in the root directory of the ISO in a file called
sns4q0.471
. SNS was used in the model numbers for the SNES and its games, and
4Q is the game code for Super Punch Out specifically. It looks like a
bog-standard SFC file, and let's be real, they probably just downladed it from
the internet, because why not. (Some people have pointed out that it looks like
a file that would have come from Nintendo. I wouldn't put it past Nintendo to
have just downloaded it in the first place though.)
I replaced the file with a Zelda 3 ROM, booted up Fight Night, navigated to Super Punch-Out in the menus, took a deep breath...
...and rejoiced as the triforce polygons appeared on screen!
As exciting as this was, it also made me question the entire project. If it's this easy, why are there no records of other people doing this? Am I really the only one who still cares about SNESticle? (I mean, it's only been like, what, 24 years since we expected it to come out!) Still, since I couldn't find anything relevant online I decided to push on.
Playing Zelda 3 on SNESticle immediately revealed an interesting quirk with the joypad emulation. The lamp didn't work. Pressing Y on the Gamecube controller instead brought up the save menu, as Select would do on the SNES. And yet, in Super Punch-Out, the Y button seemed to work like you would expect. More on this in the joypad section further down.
Getting rid of Fight Night
Replacing the SNES ROM is cool and all, but playing it involves waiting for Fight Night to load, and then going through the menus and selecting Super Punch-Out. It's also a 1.5 GB ISO, not really an acceptable size for a SNES emulator and a single ROM. Clearly, Fight Night had to go.
Game code is typically located in a section of the DVD that is not part of the
file system but still referred to as main.dol
. It is of course possible for
games to load executable code from other files, but in many cases, the code
itself is such a small part of a game that it all easily fits in main.dol. This
looks to be the case with Fight Night, and indeed, SNESticle itself is also
baked into main.dol.
The really cool thing to do would be to disassemble main.dol entirely, locate SNESticle and then build a new dol file containing just SNESticle. I decided, at least as a first milestone, I would go for a simpler hack, just patch main.dol to jump into SNESticle as soon as possible after the game is booted, and then remove as many Fight Night assets as possible from the ISO to bring the size down. Then main.dol would still contain all the Fight Night code, but it's only 5 MB in total which I think is quite acceptable.
Finding the SNESticle entry point
To be able to jump into SNESticle, the first thing we need to know is its entry point. This is relatively easy to figure out using Dolphin. Just launch Super Punch-Out and pause the emulator. Dolphin very helpfully displays the function call stack leading up to the current execution state.
By placing breakpoints at the beginning of each of these functions and then resuming emulation we can see which functions get called again and which ones do not. The ones that do get called over and over are obviously used by SNESticle and though they could be of interest later on, they can't be used as entry points. The innermost function that does not get called again must be the one that contains the main SNESticle loop. That turns out to be the function containing 0x8028de10 (which begins at 0x8028dd3c), and this looked like a promising entry point. Poking around in it in Ghidra, I also noticed that one of the functions it calls before going into the emulation loop has a hard-coded reference to the string "sns4q0.471" (the name of the ROM file), so that would almost certainly have to be the function that loads the ROM file into memory.
Booting into SNESticle
Although knowing where SNESticle lives is a good start, it's still not obvious exactly how to go about shortcutting into it. There may be initialisation functions that need to run first, and the registers need to be in the correct state when the jump is made. Really this was mostly figured out through trial and error. My initial attempts, using Dolphin to just set the PC to what looked like reasonable entry points into SNESticle were not promising. I decided to try a different approach and patch the Fight Night menu system to automatically load SNESticle without user interaction. By debugging the functions that execute just prior to SNESticle I could see that writing a 4 to 0x804ccc38 would trigger SNESticle as soon as the game got past the loading screen.
if (DAT_804ccc38 == 4) { puVar1 = &DAT_804d0000; DAT_804ccc38 = 0; DAT_804ccc20 = 0; uVar13 = extraout_f1_03; puVar3 = FUN_80171a74(extraout_f1_03,param_2,param_3,param_4,param_5,param_6,param_7,param_8, uVar9,uVar10,(int)param_11,param_12,param_13,param_14,&DAT_804d0000, param_16); uVar13 = FUN_801735e8(uVar13,param_2,param_3,param_4,param_5,param_6,param_7,param_8,(int)puVar3 ,uVar10,(int)param_11,param_12,(byte *)param_13,param_14,puVar1,param_16); snesticle_FUN_8028dd3c (extraout_f1_04,param_2,param_3,param_4,param_5,param_6,param_7,param_8, (int)((ulonglong)uVar13 >> 0x20),(int)uVar13,(int)param_11,param_12,param_13,param_14 ,puVar1,(int)param_16); }
This was easy enough to patch, and that removed the hassle of having to go through the Fight Night menus to launch SNESticle, now I just needed to bypass the asset loading, and ideally hide the loading screen entirely. Sadly, this proved difficult.
I mapped out the tree of function calls (to an arbitrary but reasonable depth) from the main function all the way up to SNESticle, and patched out every function that seemed to affect the progress bar, the idea being that I wanted to cause minimal disruption to the intended control flow of Fight Night. It actually sort of worked. The loading screen showed up, and disappeared almost immediately, and then SNESticle started. But it seemed unpredictable, Dolphin would complain about null pointer references and occasionally just crash. Unsurprisingly, it did not work on a real Gamecube. I'm sure this method could have worked with some more care put into it, but it was also unsatisfactory in that the loading screen showed up at all (however briefly). I figured I could probably locate the image on the ISO and replace it with a custom SNESticle logo, but seeing as I also had the other problems, I went back to my original approach of jumping directly into SNESticle.
This time, choosing my SNESticle entry point a little more carefully (for the record, I finally went with 0x801a0254, which is located just a few instructions before the call to the main SNESticle function, jumping closer to (or directly into) SNESticle worked fine in Dolphin but not on real hardware), and being slightly more systematic about searching for a point to jump from, I finally made some progress. I noticed that jumping into SNESticle during the loading screen was possible, but the result was that the loading continued in the background and SNESticle ran at about half speed. Clearly the jump would have to be made before loading even began. I already knew that the SNES ROM was loaded separately by SNESticle so at least that would not be affected by skipping the loading screen. After many dead ends I finally found a point (0x800ecadc), just before the loading screen where I could reliably jump into SNESticle. It caused a couple of null references, but they could be patched out seemingly without adverse effects.
Since the game has not even begun its asset loading at this point, it was now safe to remove every single file from the ISO file system, save for the SNES ROM and the banner file. Again, the fact that Dolphin can run the game from a directory rather than an ISO file is super-helpful when it comes to testing changes to the DVD filesystem.
Fixing the joypad
As already mentioned, the joypad emulation was strange. The Gamecube Y button seemed to double as Y and Select on the SNES, and Z on the Gamecube would normally exit SNESticle and go back into Fight Night, but now, with Fight Night mostly gone, it only really served to crash the game. Not cool!
Also, the mapping of buttons from Gamecube to SNES was very literal, with A, B, X and Y on the Gamecube corresponding to A, B, X and Y respectively on the SNES. That's fine for Super Punch-Out which has in-game button configuration, but it's terrible for most games. Just imagine playing Super Smash TV with the buttons rotated 90 degrees clockwise!
Finding the joypad code was not as straightforward as finding SNESticle itself. In part because I had no idea what the joypad data coming from the Gamecube hardware would look like. But I figured that whatever it looked like, the joypad state would probably be stored somewhere in memory, and if I could find that memory location I could use Ghidra or Dolphin to find code that references that location. So what I wanted to do was to find memory locations that react to joypad input, but executing just a single frame of emulation can cause huge amounts of memory to change so a slightly more sophisticated approach was needed. Eventually I came up with the following:
- Create a save state in Dolphin
- Execute one frame
- Dump the entire GC memory to a file
- Load the save state
- Hold down a button on the controller
- Execute one frame
- Dump the memory again
This will result in two very similar memory dumps, where all differences are more or less direct consequences of the button pressed. I then used memory breakpoints in Dolphin to find code that accessed these locations and eventually stumbled upon this interesting piece of code:
uVar2 = 0; if (*(int *)(iVar4 + 4) == 2) { uVar2 = *(uint *)(iVar4 + 8); local_18[0] = 0; if ((uVar2 & 1) != 0) { local_18[0] = 0x200; } if ((uVar2 & 2) != 0) { local_18[0] = local_18[0] | 0x100; } if ((uVar2 & 8) != 0) { local_18[0] = local_18[0] | 0x800; } if ((uVar2 & 4) != 0) { local_18[0] = local_18[0] | 0x400; } bVar1 = (uVar2 & 0x800) != 0; if (bVar1) { local_18[0] = local_18[0] | 0x2000; } if ((uVar2 & 0x1000) != 0) { local_18[0] = local_18[0] | 0x1000; } if ((uVar2 & 0x200) != 0) { local_18[0] = local_18[0] | 0x8000; } if ((uVar2 & 0x100) != 0) { local_18[0] = local_18[0] | 0x80; } if ((uVar2 & 0x20) != 0) { local_18[0] = local_18[0] | 0x10; } if ((uVar2 & 0x40) != 0) { local_18[0] = local_18[0] | 0x20; } if ((uVar2 & 0x400) != 0) { local_18[0] = local_18[0] | 0x40; } if (bVar1) { local_18[0] = local_18[0] | 0x4000; } if ((local_18[0] & 0x200) != 0) { local_18[0] = local_18[0] & 0xfeff; } if ((local_18[0] & 0x400) != 0) { local_18[0] = local_18[0] & 0xf7ff; } uVar2 = *(uint *)(iVar4 + 8) >> 4 & 1; } if (param_9[10] == 0) { iVar4 = *(int *)(*param_9 + 8); (**(code **)(iVar4 + 0x2c))(*param_9 + (int)*(short *)(iVar4 + 0x28),local_18,0,param_9[6],1); } else { iVar4 = *(int *)(*param_9 + 8); (**(code **)(iVar4 + 0x2c)) (*param_9 + (int)*(short *)(iVar4 + 0x28),local_18,param_9[3],param_9[6],1); } return uVar2;
And the corresponding disassembly:
8028e560 39 20 00 00 li r9,0x0
8028e564 7c 0b 03 78 or r11,r0,r0
8028e568 70 0a 00 01 andi. r10,r0,0x1
8028e56c 41 82 00 08 beq LAB_8028e574
8028e570 39 20 02 00 li r9,0x200
LAB_8028e574
8028e574 71 60 00 02 andi. r0,r11,0x2
8028e578 41 82 00 08 beq LAB_8028e580
8028e57c 61 29 01 00 ori r9,r9,0x100
LAB_8028e580
8028e580 71 6a 00 08 andi. r10,r11,0x8
8028e584 41 82 00 08 beq LAB_8028e58c
8028e588 61 29 08 00 ori r9,r9,0x800
LAB_8028e58c
8028e58c 71 60 00 04 andi. r0,r11,0x4
8028e590 41 82 00 08 beq LAB_8028e598
8028e594 61 29 04 00 ori r9,r9,0x400
LAB_8028e598
8028e598 71 60 08 00 andi. r0,r11,0x800
8028e59c 4f 80 00 00 mcrf cr7,cr0
8028e5a0 41 9e 00 08 beq cr7,LAB_8028e5a8
8028e5a4 61 29 20 00 ori r9,r9,0x2000
LAB_8028e5a8
8028e5a8 71 6a 10 00 andi. r10,r11,0x1000
8028e5ac 41 82 00 08 beq LAB_8028e5b4
8028e5b0 61 29 10 00 ori r9,r9,0x1000
LAB_8028e5b4
8028e5b4 71 60 02 00 andi. r0,r11,0x200
8028e5b8 41 82 00 08 beq LAB_8028e5c0
8028e5bc 61 29 80 00 ori r9,r9,0x8000
LAB_8028e5c0
8028e5c0 71 6a 01 00 andi. r10,r11,0x100
8028e5c4 41 82 00 08 beq LAB_8028e5cc
8028e5c8 61 29 00 80 ori r9,r9,0x80
LAB_8028e5cc
8028e5cc 71 60 00 20 andi. r0,r11,0x20
8028e5d0 41 82 00 08 beq LAB_8028e5d8
8028e5d4 61 29 00 10 ori r9,r9,0x10
LAB_8028e5d8
8028e5d8 71 6a 00 40 andi. r10,r11,0x40
8028e5dc 41 82 00 08 beq LAB_8028e5e4
8028e5e0 61 29 00 20 ori r9,r9,0x20
LAB_8028e5e4
8028e5e4 71 60 04 00 andi. r0,r11,0x400
8028e5e8 41 82 00 08 beq LAB_8028e5f0
8028e5ec 61 29 00 40 ori r9,r9,0x40
LAB_8028e5f0
8028e5f0 41 9e 00 08 beq cr7,LAB_8028e5f8
8028e5f4 61 29 40 00 ori r9,r9,0x4000
LAB_8028e5f8
8028e5f8 71 2a 02 00 andi. r10,r9,0x200
8028e5fc 41 82 00 08 beq LAB_8028e604
8028e600 71 29 fe ff andi. r9,r9,0xfeff
LAB_8028e604
8028e604 71 20 04 00 andi. r0,r9,0x400
8028e608 41 82 00 08 beq LAB_8028e610
8028e60c 71 29 f7 ff andi. r9,r9,0xf7ff
LAB_8028e610
8028e610 b1 21 00 08 sth r9,8(r1)
8028e614 80 03 00 08 lwz r0,0x8(r3)
8028e618 54 1f e7 fe rlwinm r31,r0,0x1c,0x1f,0x1f
LAB_8028e61c
8028e61c 80 1e 00 28 lwz r0,0x28(r30)
8028e620 2c 00 00 00 cmpwi r0,0x0
8028e624 41 82 00 34 beq LAB_8028e658
8028e628 80 7e 00 00 lwz r3,0x0(r30)
8028e62c 38 81 00 08 addi r4,r1,0x8
8028e630 80 de 00 18 lwz r6,0x18(r30)
8028e634 38 e0 00 01 li r7,0x1
8028e638 81 23 00 08 lwz r9,0x8(r3)
8028e63c 80 be 00 0c lwz r5,0xc(r30)
8028e640 a8 09 00 28 lha r0,0x28(r9)
8028e644 81 29 00 2c lwz r9,0x2c(r9)
8028e648 7c 63 02 14 add r3,r3,r0
8028e64c 7d 28 03 a6 mtspr LR,r9
8028e650 4e 80 00 21 blrl
8028e654 48 00 00 30 b LAB_8028e684
LAB_8028e658
8028e658 80 7e 00 00 lwz r3,0x0(r30)
8028e65c 38 81 00 08 addi r4,r1,0x8
8028e660 80 de 00 18 lwz r6,0x18(r30)
8028e664 38 a0 00 00 li r5,0x0
8028e668 81 23 00 08 lwz r9,0x8(r3)
8028e66c 38 e0 00 01 li r7,0x1
8028e670 a8 09 00 28 lha r0,0x28(r9)
8028e674 81 29 00 2c lwz r9,0x2c(r9)
8028e678 7c 63 02 14 add r3,r3,r0
8028e67c 7d 28 03 a6 mtspr LR,r9
8028e680 4e 80 00 21 blrl
LAB_8028e684
8028e684 7f e3 fb 78 or r3,r31,r31
8028e688 80 01 00 24 lwz r0,36(r1)
8028e68c 7c 08 03 a6 mtspr LR,r0
8028e690 bb c1 00 18 lmw r30,24(r1)
8028e694 38 21 00 20 addi r1,r1,0x20
8028e698 4e 80 00 20 blr
Turns out the joypad state data on the Gamecube (at least the way it's presented to SNESticle) is pretty similar to its SNES counterpart. It's just a bit field where each bit corresponds to a single button. Not surprising really, as the two systems both have exactly 12 buttons (and the GC of course also has a bunch of analogue inputs but we don't care about those (except L and R, but their values have already been converted to single bits at this point)). The exact mapping between buttons and bits differs heavily between the two systems, though, so this function translates the Gamecube state into a SNES state, and it's really quite fortunate that it exists, or remapping the buttons would have required a lot more work.
These are the bitmask values corresponding to the buttons on the two systems:
GC value | button | SNES value |
---|---|---|
0x0001 | Left | 0x0200 |
0x0002 | Right | 0x0100 |
0x0004 | Down | 0x0400 |
0x0008 | Up | 0x0800 |
0x0010 | Z | |
0x0020 | R | 0x0010 |
0x0040 | L | 0x0020 |
0x0100 | A | 0x0080 |
0x0200 | B | 0x8000 |
0x0400 | X | 0x0040 |
0x0800 | Y | 0x4000 |
0x1000 | Start | 0x1000 |
Select | 0x2000 |
This code also explains why Y doubles as Y and Select. At address 0x8028e59c, a flag in cr7 is set if bit 11 (0x800, Y) of the GC joypad state is set. This flag is then immediately tested and if it is set, bit 13 (0x2000, Select) of the SNES joypad state is set:
8028e598 71 60 08 00 andi. r0,r11,0x800
8028e59c 4f 80 00 00 mcrf cr7,cr0
8028e5a0 41 9e 00 08 beq cr7,LAB_8028e5a8
8028e5a4 61 29 20 00 ori r9,r9,0x2000
Further down, at address 0x8028e5f0, the flag is tested for again and if it is set, bit 14 (0x4000, Y) of the SNES joypad state is set:
8028e5f0 41 9e 00 08 beq cr7,LAB_8028e5f8
8028e5f4 61 29 40 00 ori r9,r9,0x4000
It's probably no surprise but I think it's worth pointing out that there is no
way this is hand-written assembly code. A human would just have written
ori r9, r9, 0x6000
to set both Select and Y at once in the SNES state, but the
compiler wasn't that clever. It did, however, notice that there were two checks
for bit 11 (0x800) in the GC state and performed a simple optimisation known as
common subexpression elimination by caching the result of the check in the cr7
register rather than performing it twice. (The Ghidra decompiler can't undo this
optimisation and instead uses the variable bVar1 to cache the result.) The fact
that the compiler wasn't very good at optimising (or just wasn't running with
aggressive optimisation flags) turned out to be a blessing in disguise, though.
Remapping the buttons to un-rotate the face buttons is trivial, it's just a matter of replacing the constant operands in the ori instructions. Unmapping Select from Y is also quite simple, patching in a nop on line 0x8028e5a4 does the job.
At address 0x8028e618, a r31 is set to 1 if Z (0x10) is pressed and this is what causes the emulator to exit.
8028e618 54 1f e7 fe rlwinm r31,r0,0x1c,0x1f,0x1f
This can also be prevented with a nop.
Then came the fun part! I still needed to map Select to something, and the Z button was really the only choice, but each button translation would seem to require three consecutive instructions. The code for Select should look something like this:
andi. r0, r11, 0x10
beq skip_next_instruction
ori r9, r9, 0x2000
But there's no obvious place to put it. Some small optimisation is needed in order to make room for this new code. Luckily, the function is really poorly optimised to begin with and there are many ways to shorten it down. The first thing I thought of was that the L and R bits in the joypad state are adjacent in the Gamecube state as well as in the SNES state. That means we could process them together by adding up their bit masks and then rotating them into the position they have on the SNES, like this:
andi. r0, r11, 0x60
beq skip_next_two_instructions
rlwinm r0, r0, 0x1f, 0x1a, 0x1b
or r9, r9, r0
That would save two instructions, which should be enough since I could also overwrite some of the recently deactivated code. But actually, the andi and the beq are superfluous here. rlwinm is a stupidly powerful instruction that performs rotation and masking in one go and the whole thing could be shortened to:
rlwinm r0, r11, 0x1f, 0x1a, 0x1b
or r9, r9, r0
This would rotate the Gamecube joypad state 31 bits to the left (ie one bit to the right), and mask out everything but bits 4 and 5 (0x1a through 0x1b when counting from the MSB) and store the result in r0 (and then or that into r9). The same trick could be used all over the place to save one instruction on every button translation.
And then I found out there's an even more awesome instruction, rlwimi, that will even write the bits to r9:
rlwimi r9, r11, 0x1f, 0x1a, 0x1b
But none of this was needed because there's an even simpler way! The Start button sits at bit 12 (0x1000) in both representations, so instead of starting out by setting the SNES joypad state to 0, we could just set it to whatever the Start button state is on the GC by patching a single instruction, from:
8028e560 39 20 00 00 li r9,0x0
to:
8028e560 70 09 10 00 andi. r9, r0, 0x1000
This makes the Start button translation further down completely redundant and we can just modify that code to check for Z and map it to Select instead.
In the end, the button mapping looks like this:
GC | SNES |
---|---|
A | B |
B | Y |
X | A |
Y | X |
Start | Start |
Z | Select |
One final interesting thing to note about this joypad code is the following bit right at the end (comments added by me):
if ((local_18[0] & 0x200) != 0) { // If Left is pressed
local_18[0] = local_18[0] & 0xfeff; // Clear the Right bit
}
if ((local_18[0] & 0x400) != 0) { // If Down is pressed
local_18[0] = local_18[0] & 0xf7ff; // Clear the Up bit
}
Anyone who has programmed a game (or an emulator) for a computer will know what this is about. What happens if you press Left and Right at the same time? Sardu's solution is that Left always overrides Right, and Down overrides Up. What makes it interesting is that it's unnecessary on a Gamecube. The Gamecube controller will prevent this from happening in exactly the same way the SNES controller would. So from an emulator accuracy standpoint, it would arguably be best to just leave this code out. It's not much but it's a tiny little hint that this code was originally written with keyboard input in mind.
The banner
The banner is the 96x32 bitmap used to represent a game in the Gamecube OS, in custom loaders like Swiss, and in emulators. SNESticle obviously deserves a banner of its own. The Fight Night Round 2 banner is not just misleading now that we've more or less deleted Fight Night from the disc, it's also just a really ugly banner!
Seriously, what were they thinking?
I initially thought of modding some old NESticle logo, and though I doubt anyone at Bloodlust would really care, I decided against it for legal reasons. My next idea was this little banner, using the NESticle colour scheme and font:
But it turned out that some loaders display only the banner in their menus, and having all of your SNES games show up as just "SNESTICLE" is no good, so I took it a step further and the default behaviour is now to generate a banner using the provided game name (or the SNES ROM file name if no game name is provided). This is what it looks like for Super Punch-Out:
The script still has an option to create a banner from any image file, so of course nothing is stopping you from replacing it with something like Shitman or Buddy.
Since I couldn't find good utility for creating banners from png images I wrote
my own. It's in the git repo and
it's called a2bnr.py
. It's used as a module by fn22snesticle.py
but it's also
a standalone program. See the README.md file for usage.