One tried and true method for hacking ROMs is intercepting the controller input. If you can figure out where the game is inputting controls, then you can do a variety of things like enable new buttons not previously handled by the game, pulse a button on and off (for rapid fire, etc.), add sound f/x when a button is pressed, and so on.
For the Atari Lynx, this is accomplished by accessing two hardware addresses:
JOYSTICK .eq $FCB0 SWITCHES .eq $FCB1
The joypad and most buttons are all mapped into single bits in JOYSTICK; only the lonely Pause button is mapped into SWITCHES. If you wanted to read the JOYSTICK in assembly, it would probably look like this:
lda JOYSTICK ; Assembled: ADB0FC
Note I included the assembled output of the load instruction, which is $ADB0FC. Since most Lynx games do not compress their code, one can easily do a search within the ROM file looking for that hexadecimal string. If you do that, you’ll likely find multiple instances of this string. Why? Because most games have title screens, option menus, high score screens, and the actual game itself. Rather than have one routine which always handles joypad input, it’s fairly common for games to load a custom joypad handler for each of those separate game segments. However, that was not the case in Quadromania. I only found one hit in the ROM, which meant there was a single complicated state machine controlling what to do based on the current screen presented to the player. It also meant some things were universal, like the Pause button — the original ROM always displayed the PAUSE message when the Pause button was pressed, regardless of what screen is shown. Not a bug per se, but not a very slick UX, either. So let’s see if we can fix that.
Normally, without any source, you’d have to find a spare byte of RAM (which for the Lynx would maybe be near the end of the ZPAGE, or somewhere between end of code + data but before the screen buffers). You could then create a “pause_ok” variable in that spot which your patch code could use to determine if it’s OK to support the pause function or not. That would be a perfectly fine approach. However, with my partial source code, I was able to determine there was already a no_pause variable in the Quadromania code. It wasn’t widely used, thus it looked pretty safe for me to take advantage of it as defined.
What I ended up doing was hacking the startup code (right after boot), the “get ready” screen, and the “game over” screen. By initializing the no_pause variable to 1 right after boot, then pause won’t be supported on the first display of the title screen. In the get ready screen, I re-enable pause right before the level starts. And then in the game over screen, I immediately disable pause before it displays, which will keep pause disabled until the next game starts with the “get ready” screen. This was actually really straightforward, and took almost no code beyond storing either a zero or a one into the no_pause variable in a patch subroutine. It’s also fairly specific to Quadromania, so I won’t bother to show that code here since it doesn’t really have value apart from that ROM.
What if you didn’t have source, and your binary search yielded multiple results? That’s a little bit of a tougher nut to crack, but not impossible. You need to figure out which read of the joypad is near the part of the code you want to change. For example, let’s say you want to add a new options menu when the player presses Opt 1 on the title screen, assuming Opt 1 is currently ignored in the ROM you want to hack. You’ve found perhaps 3 instances of the $ADB0FC string, so you’ve got two choices:
- Hack each instance one at a time, and see if it detects your newly-created Opt 1 patch subroutine on the proper screen.
- Use the Handy integrated debugger, which allows you to set breakpoints on addresses.
#2 can be tricky for a multi-load game, because you may not be sure which chunk of the ROM gets loaded to what location in RAM. You can try to reverse-engineer the ROM directory as I discussed in part one, but again that will require some trial-and-error if you don’t really know which entries are being loaded to RAM at what times. But the good news is you’ve got a RAM load address in each directory entry. You can use this load address as the base address for a given directory chunk of bytes, then use the offset of the $ADB0FC string within that chunk of bytes to figure out the corresponding offset in RAM to set a breakpoint.
Directory entry #2 says it loads at $800 in RAM and is $2000 bytes long.
ROM block and offset for entry #2 is calculated out to be $3204 bytes into the ROM.
$ADB0FC string is located at $3381 bytes into the ROM.
$3381 – $3204 = $17D offset
RAM address for breakpoint: $800 + $17D offset = $97D
Setting a breakpoint at address $97D with the Handy debugger should allow you to catch the first occurrence of reading the JOYSTICK within that RAM load, then hopefully by watching the viewer window you can see visually what part of the game you’re in — title screen, menu, etc.
Again, when hacking the ROM, you need to do some trickery like I’ve shown in my previous posts where you replace an instruction (such as the reading of the joypad, which we just found in ROM) with a jsr to a new patch routine address. In that patch routine, you can detect instantaneous button presses, or maybe do something more complicated like track edge detection. Edge detection is when an input was previously off or on, then switches to the opposite state. This requires extra state information stored in RAM by you, of course, because that’s the only way you can know the state of the inputs from the prior time the routine was invoked.
Back to Quadromania: I was able to successfully hack the code to disable pause on the appropriate screens, with no ill effects on the rest of the code. I even added a spot to detect Opt 1 + Opt 2 to erase the EEPROM.
Feature #5: SUCCESS
Changing gears for a bit, I did fix a couple of key bugs in Quadromania. It’s amazing what you can find after many hours of studying the partial source as well as disassembling the ROM. One bug I found by code inspection, and one I found while playing.
Quadromania has 20 levels, and a pretty steep difficulty curve when you get to the higher levels. Thus I was not likely to reach the end of the game on my own. However, by code inspection I noticed there was a check for max_level, and if the max level was defeated, it wanted to wrap back to zero. The only problem is that zero is not actually used in the game; the lowest level starts with a value of 1. I hacked the code to let an easier level be the max, like level 3. Sure enough, if I beat level 3, the game locked up on me. If I changed the code to roll back to level 1, then it worked. I don’t think the game ever really intended for the player to get past level 20, but I made this fix just to be safe.
The second bug I found during gameplay. It turns out there is a condition where a falling block could wrap from the bottom of the screen back to the top. This event actually causes a RAM corruption, which means the game may or may not lock up at some later point after this happens. I studied the partial source and it sure looked like it was handling all four edges of the playfield boundaries correctly, so I was stumped for a long time. Eventually I started assembling some of those bits of boundary detection and searching for the strings in the ROM file. To my surprise, the check for the bottom edge of the playfield was not in the ROM! This is when I realized I couldn’t completely trust the source in front of me, as the source had a fix not present in the ROM. Even though this process took several hours of deduction, the good news is the fix was easy to apply since the original author had apparently found it in his own testing.
After putting both fixes in place, and playing the game for many hours across all 20 levels of puzzles, I have not encountered any additional bugs.
Feature #6: SUCCESS
Next time, we’ll look at hacking the levels and speed, and how my oldest son gave me a tool which made it all extremely fast to prototype! I might even talk about how to use Python to apply ROM patches, which was another big time-saver I discovered halfway through this project.