loading...
All NES articles | Back to top

Kung Fu ROM-hack

Kung Fu (NES)
Kung-Fu Master (Arcade)

My friends Peter and Carrie are getting married in the US, and some of Peter's friends are making a video for the wedding. I had randomly just watched a video of NES game 'Kung Fu', and noticed the ending where 'THOMAS' rescues 'SYLVIA' and there are little blinking hearts around them. Very cute.

I somehow got it into my head that I would create a hacked ROM of Kung Fu and record a video of it with the names 'THOMAS' and 'SYLVIA' replaced with 'PETER' and 'CARRIE'.

Wheels on Meals

Spartan X Arcade Flyer (1984)

'Kung Fu' is a port of Irem's 1984 arcade game 'Kung-Fu Master', released as 'Spartan X' in Japan, and based on the 1984 martial arts comedy movie Wheels on Meals (快餐車) with Jackie Chan.

Wheels on Meals (1984)

The game has you ascending a tower with five levels to face the final boss at the top. This scenario is very reminiscent of Bruce Lee's incomplete film from 1972, 'Game of Death', where he ascends five levels of a tower to fight different opponents.

Searching for Text

Ending
Kung Fu NES Cartridge

On the game completed screen it says 'CONGRATULATIONS'. I wanted to see if I could find this anywhere in the ROM. Firstly, I opened a ROM dump in the hex editor HxD, and searched the 40 KB of data for the ASCII string 'CONGRATULATIONS'. No luck.

I still assumed that the text was present in the ROM, though apparently not in ASCII encoding. Another encoding would probably still retain the normal alphabetical ordering of letters. So if 'A' was encoded as value x, 'B' would have value x+1, etc. The word 'CONGRATULATIONS' would have a structure like this:

C   O   N   G   R   A   T   U   L   A   T   I   O   N   S
2   14  13  6  17   0   19  20  11  0   19  8   14  13  18

I would only need to search through the ROM dump, looking for any numeric sequence:

x+2,  x+14, x+13, x+6, x+17,  x+0, x+19, x+20,
x+11, x+0,  x+19, x+8, x+14, x+13, x+18

For the 8-bit NES, it is reasonable to assume that the characters have 8-bit numbers, so x can only be in the range 0-255. When adding x and the alphabetical indices as 8-bit numbers, values above 255 would be wrapped around to 0.

Given this wrap-around, there was no reason to not just add x to the ASCII values of the characters instead of the alphabetic indices listed above, since we're trying every 8-bit value of x anyway. So we'll use the ASCII hexadecimal values:

 C   O   N   G   R   A   T   U   L   A   T   I   O   N   S
 43  4f  4e  47  52  41  54  55  4c  41  54  49  4f  4e  53 

I wrote a little C++ program that loaded the ROM into memory:

const int DATA_SIZE = 40976;
char data[DATA_SIZE];
ifstream infile("kungfu.nes", ios::in | ios::binary);
infile.read(data, DATA_SIZE);

And did a brute force search for the string 'CONGRATULATIONS' where the ASCII values are offset with every possible value of x:

search_string = "CONGRATULATIONS";
search_size = strlen(search_string);

// for all values of x
for (unsigned char x = 0; x < 255; ++x)
{
    // for all ROM bytes
    for (int i = 0; i < DATA_SIZE - search_size; ++i)
    {
        bool is_match = true;

        // for every character in 'CONGRATULATIONS'
        for (int c = 0; c < search_size; ++c)
        {
            unsigned char rom_byte = data[i + c];
            unsigned char string_byte = search_string[c] + x;
            if (rom_byte != string_byte)
            {
                is_match = false;
                break;
            }
        }
        if (is_match)
        {
            // we got a match
        }
    }
}

I ran the search, and heureka! We got a match starting at byte offset $1A66 when x = 201:

                        C  O  N  G  R  A  T  U  L  A  T  I  O  N  S 
Search string (ASCII) : 43 4f 4e 47 52 41 54 55 4c 41 54 49 4f 4e 53 
ROM                   : 0c 18 17 10 1b 0a 1d 1e 15 0a 1d 12 18 17 1c 

So 'A' has the value $0a, B the value $0b, etc.

Just to verify, let's check that the offset x = 201 is added to the ASCII values of the characters gives the ROM values above. For 'C', we have the ASCII value $43 (67). If we add x = 201, we get:

$43 + x = 67 + 201 = 268

This number is larger than 255, so during 8-bit addition it is wrapped to the value:

268 % 256 = 12 = $0c

Which is indeed the value in the ROM corresponding to 'C'.

Replacing Text

Title screen: original

The search routine was quickly expanded into having replacement functionality. When we have our x value, we can offset any string to fit the scheme in the ROM. The first test was to change the word 'NINTENDO' on the title screen.

The modified data was written out to a new file:

ofstream myFile ("hacked.nes", ios::out | ios::binary);
myFile.write ((const char *)data, DATA_SIZE);
myFile.close();

I could launch the hacked ROM in the Mednafen emulator without problems, and 'NINTENDO' was indeed changed on the title screen.

Title screen: hacked

This was my first successful NES ROM hack!

The next obvious candidate was the 4 main menu options:

1 PLAYER GAME A
1 PLAYER GAME B
2 PLAYER GAME A
2 PLAYER GAME B

I tried searching for the string '1 PLAYER GAME A' and didn't find anything. There were several reasons for that:

The first issue was that spaces were not in the same position relative to the other letters as in ASCII. Not very surprising, no reason why it would be. The simplest solution in the search function was to ignore spaces in the search string completely and just move on to the next character.

Title screen: hacked more

When ignoring spaces and searching for the string '1 PLAYER GAME A' it still didn't find anything. Another new feature here was the number '1'. As with the space, there was no reason for the numbers to be in the same position relative to the letters as in ASCII. I removed the '1' from the searching string, and yes, 'PLAYER GAME A' did indeed show up in the ROM, and could be replaced.

However, 'PLAYER GAME B' did not show up in the ROM, which first led me to incorrectly assume that the string was copied from 'PLAYER GAME A' and the last letter modified. But when I changed the string 'PLAYER GAME A' to something new, 'PLAYER GAME B' did not change.

Kung Fu Tileset

It turned out that 'B' was not in its position next to 'A', instead there was a weird star character. I ended up using Mednafen's debug mode (Alt+d and Alt+2) to output the character map loaded for Kung Fu. It turned out that 'B' indeed was not next to 'A', but rather, the character map looked like this:

00  0123456789A CDEF
10  GHI.-LMNOP RSTUV
20    Y
30 
...
E0 
F0          X B     

Apart from 'B' being in the wrong place, so was 'X', and the characters 'JKQWZ' were missing entirely. This is a fun bit of trivia: The NES 'Kung Fu' ROM doesn't have the letter 'K' in it, so the words 'Kung Fu' are never written out in the game. I guess the original Japanese title was 'Spartan X', which you could write using the letters in the ROM.

The missing characters put a few limitations on what I could write, but after spending some time to think about it, I came up with some fun substitutions, these among others:

do_replace(data, DATA_SIZE, "PLAYER", "6PETER"); // '6' is black
do_replace(data, DATA_SIZE, "THOMAS", "PETER ");
do_replace(data, DATA_SIZE, "SYLVIA", "CARRIE");
do_replace(data, DATA_SIZE, "IREM CORP ", "ARTSY ELF ");
do_replace(data, DATA_SIZE, "1984", "2018");
Hacked ending

And the results of replacing them:

  Searching for 'PLAYER'...
  MATCH at $017a using char offset 201
  - Search string        : 50 4c 41 59 45 52 
  - ROM                  : 19 15 0a 22 0e 1b 
  - Replacement string   : 36 50 45 54 45 52 
  - Replacement (offset) : ff 19 0e 1d 0e 1b 
  MATCH at $16ae using char offset 201
  - Search string        : 50 4c 41 59 45 52 
  - ROM                  : 19 15 0a 22 0e 1b 
  - Replacement string   : 36 50 45 54 45 52 
  - Replacement (offset) : ff 19 0e 1d 0e 1b 
  ...
  
  Searching for 'THOMAS'...
  MATCH at $1a45 using char offset 201
  - Search string        : 54 48 4f 4d 41 53 
  - ROM                  : 1d 11 18 16 0a 1c 
  - Replacement string   : 50 45 54 45 52 20 
  - Replacement (offset) : 19 0e 1d 0e 1b fc 
  
  Searching for 'SYLVIA'...
  MATCH at $1a28 using char offset 201
  - Search string        : 53 59 4c 56 49 41 
  - ROM                  : 1c 22 15 1f 12 0a 
  - Replacement string   : 43 41 52 52 49 45 
  - Replacement (offset) : 0c 0a 1b 1b 12 0e 
  MATCH at $1a4f using char offset 201
  - Search string        : 53 59 4c 56 49 41 
  - ROM                  : 1c 22 15 1f 12 0a 
  - Replacement string   : 43 41 52 52 49 45 
  - Replacement (offset) : 0c 0a 1b 1b 12 0e 
  
  Searching for 'IREM CORP '...
  MATCH at $16f4 using char offset 201
  - Search string        : 49 52 45 4d 20 43 4f 52 50 20 
  - ROM                  : 12 1b 0e 16 fc 0c 18 1b 19 13 
  - Replacement string   : 41 52 54 53 59 20 45 4c 46 20 
  - Replacement (offset) : 0a 1b 1d 1c 22 fc 0e 15 0f fc 
  
  Searching for '1984'...
  MATCH at $16fe using char offset 208
  - Search string        : 31 39 38 34 
  - ROM                  : 01 09 08 04 
  - Replacement string   : 32 30 31 38 
  - Replacement (offset) : 02 00 01 08 

The numeric example is special, as it has an offset of 208 unlike the other strings. I didn't actually need to know where numbers were located relative to characters, as long as I only search and replaced numbers.

Now the modified ROM was done, and I could focus on recording a video using Mednafen.

Kung Fu Cheating

The final challenge is that I'm no good at Kung Fu. But since we're already hacking ROMs, I thought that I might as well try to cheat as well.

I consulted thealmightyguru.com for cheats and found a couple I could use:

Address $04A5 - Boss   Hit Points (values $00-$30)
Address $04A6 - Player Hit Points (values $00-$30)

So, a way to make your character invincible is to set $04A6 = $30 every time the value is read. In Mednafens cheat interface, you can do it like this:

Mednafen cheat interface

First, Alt+c enters the cheat interface, then:

  > 2 # Cheat Search...
  > 1 # Add Cheat
  Name: hits
  Type: S (Substitue on reads)
  Address: $4a6
  Value: $30
  Enable? Y

And finally, Alt+c to exit the cheat interface. This totally worked, except for when a level ends and the hit points are converted into bonus points. This will go on forever, because the cheat resets the value to $30 every time it is read. So, when a level ends, the cheat should be temporarily disabled.

The final boss was a bit tricky as well, so I set his hitpoints to $01 and then disabled the cheat to defeat him with a single low kick. Disabling the cheat was necessary to enable his hit points to go below $01 when I kicked him.

When the cheat interface is running, the game is paused, and I could toggle cheats even while recording footage for the wedding video.

I recorded to QuickTime using:

mednafen -qtrecord hacked.mov hacked.nes

References