Skip to main content

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...

Zelda 3 on SNESticle

...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.

Dolphin callstack

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.

Fight Night Round 2 loading screen

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:

  1. Create a save state in Dolphin
  2. Execute one frame
  3. Dump the entire GC memory to a file
  4. Load the save state
  5. Hold down a button on the controller
  6. Execute one frame
  7. 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!

Ugly Fight Night Round 2 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:

SNESTICLE banner

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:

Super Punch-Out!! banner

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.