Adding Free Play to Vs. Tetris

June 20th 2021, 6:20 pm

About a week ago I bought a Vs. Tetris kit for my Vs. DualSystem on a whim to replace Vs. Super Mario Bros. When it arrived, I swapped the ROMs and PPU and booted it up only to find that there was no free play option in the game. That presented a problem since the coin slots and service buttons aren't hooked up in my cabinet. It was completely gutted when I got it and I never hooked them up. I also prefer not to have to coin up games before playing them. So I looked online to see if anyone had made a free play mod for the game. I found nothing but other people complaining about the same thing so I decided to add it myself!

The general theory of adding free play to an arcade game that doesn't have it is simple. You figure out what routines handle coin checks and either modify them to require zero coins or trick them into thinking there are always coins inserted into the game. Nice-to-haves include displaying "FREE PLAY" instead of a coin counter on the screen, making sure the game still runs the attract sequence when it thinks its coined up and allowing the modification to be enabled or disabled, usually with an unused DIP switch. The older the game, usually the easier it is to find and fix up the routines. The game came out in 1988 and runs on modified NES hardware so I knew I wasn't going to be dealing with threads, obfuscation or compression schemes. With that in mind I set to work.

Knowing nothing about the game's internals and only a little bit about the NES layout in general, I booted the game up in MAME with the debugger enabled. I also pulled up a copy of the source to the MAME driver so I could get an overview of the Vs. DualSystem memory layout. Its often difficult to tell what the entrypoint of a ROM-based game is from MAME source so the easiest thing for me to do is to single step once in the MAME debugger and see what address it starts at. The source code will at least tell you where in memory the ROMs will be located, so its fairly easy to figure out what offset in the ROMs is the start of execution. With the start address and the ROM locations understood, I combined the ROMs into one large file and imported them into a new Ghidra project. Ghidra can load and decompile raw 6502 binaries as long as you know what address to start with, so I headed to the entrypoint offset and told Ghidra to start decompiling.

Ghidra can be a bit fussy with global variable references pointing at memory that's outside of the ROM region. For that reason, I find it is often convenient to add more of the memory map for whatever system I'm analyzing. So, I opened the Memory Map window and added the system RAM as well as the coin counter and DIP switch registers. I then started poking around to see if any obvious code popped out to me. The game initialization and main loop were pretty obvious but aside from that I couldn't see anything clearly handling coins. So, I headed back to MAME.

MAME's cheat engine is a fantastic way to find memory addresses of interest even when you aren't trying to cheat. I wanted to isolate any memory addresses that held coin counts so I could examine code that read and wrote coin values. So I initialized the cheat engine with the cheatinit debugger command, then incremented the coin counter by pressing the service credit button. I then ran cheatnext increase, 1 to tell MAME that I expected the memory I cared about to have gone up by 1. It narrowed down to a few hundred entries. So, I ran cheatnext equal without adding another coin to tell MAME that I didn't modify anything and to narrow down the memory further by keeping only the memory locations that hadn't changed. After a few iterations of adding coins and narrowing down memory I had three different addresses to look at which was good enough. I popped open three different memory viewer windows in the MAME debugger and set each one to look at a different address and tried modifying them to see what happened. The first one immediately reset itself back to the old value whenever I changed it. The second, when changed, caused the first one to mirror it. The credits display on the screen also immediately updated to display the new value I typed as well. The third one had no discernable effect.

Intuition told me the second one was probably what I cared about. So I set a memory watchpoint on the address and then performed a few actions in-game, such as adding a coin, starting a game, losing a game, etc. Basically I was allowing the game to naturally increment and decrement the coin counter and noting down the memory addresses of the code doing the actual incrementing and decrementing. I navigated to those addresses in Ghidra to check out the functions further. I'll spare the boring details of examining all of the functions I found. I ended up with a few functions that all did something with the coin count memory address. One was in the main video update loop and read the coin value to display it on the screen. This one turned out to be what was responsible for writing the coin value in the first address that I found. One was just a simple increment function for when a coin was inserted. One checked how many coins were in the machine and used that to determine whether to go back to the attract sequence or the start screen at the end of a game. And one looked at the game mode selection that a user made (1P, 2P cooperative, 2P competitive) and subtracted the right number of coins (1, 2 or 2 coins respectively) when a game was started.

With that, I had almost everything I needed in order to do the hack. Some experimenting with the "CREDITS X" display function showed me that it was writing characters to a buffer that included the X and Y position of each character and that the game only allowed a maximum of 8 characters to be written. Instead of trying to figure out how to enlarge that buffer, I decided that my free play display would print "FREEPLAY" instead of "FREE PLAY". Given each character is accompanied by an X position I could go back and adjust the display to include a space but it didn't occur to me to do that at the time. In order to modify the minimum amount of game code, I also decided to go the route of forcing the coin value to 2 (so both 1P and 2P games would work) instead of attempting to modify the various coin functions to accept 0 coins. I also decided on using "DIP 5" as the freeplay/coin mode switch as it was unused according to MAME source, various online documentation and a precursory search of code that accessed the DIP switch registers.

The first function that I wrote was the function that would display "FREEPLAY" on the screen. I started with that because it was almost a carbon copy of the "CREDITS X" display function. I just had to change the memory addresses for the text and X offsets, get rid of the bit that copied the credits amount and adjust the X offsets to center the display horizontally. Also, since this function was called in the main display loop for every frame it was a good place to stick a bit of code that would force the credit count to 2. I also wrote a small function that would load the DIP switch register, check if DIP 5 was set or cleared and then jump to my freeplay display function or the original credit display function depending on the result. Both of these functions were stuck near the end of the last ROM in a large chunk of blank space. I located where in the main thread the original credit display function was called using Ghidra and then changed it to instead call my new function that checked DIP switches and jumped to the appropriate display.

After confirming that swapping DIP 5 to "on" in MAME got the word "FREEPLAY" displayed and let me start a 1P or 2P game, I got started on the second half of the hack. When you get a game over the game checks to see if you added another credit. If you did, it goes right back to the start game screen where you can select the game type. If you did not, it goes back to the attract sequence. Since this is an arcade game running on a CRT I didn't want any unnecessary burn-in, and I wanted it to feel like it was always intended to have free play. So I went back to the function that looked at the credit count to determine whether to go back to the attract sequence or not. It was using a 3 byte 6502 opcode to load the coin count into the A register. Luckily, unconditional jumps are also 3 bytes! So I wrote another small function that checked whether DIP 5 was set or cleared and then either loaded the coin count into the A register or set the A register to zero coins. Basically, when the game is in free play mode, I lie to the function that there are no credits in this one scenario so that I can guarantee the game goes back to attract mode. In coin mode, the game will do what it was originally doing. I then replaced the original credit load with a jump to that function and tested it again in MAME.

With all that tested, it was time to burn ROMs and test it in my actual cabinet. The original ROM chips for the game had no seal over the erase windows and the game is extremely common so I wasn't worked up about reusing the ROMs. So I popped them into my UV eraser and rewrote them with the patched ROMs that I tested in MAME and popped them back into the cabinet. Success! With DIP 5 turned on, the game is now in free play mode and I can play the game!

If you want to apply the patches to your own copy of the game or you're just curious and want to see the source code, I put it all on GitHub here. The code was so simple and 6502 is so easy to work with that I did not bother setting up a 6502 assembler. Instead, I used this 6502 reference and hand-assembled the instructions that I needed. I'm probably going to go back sometime this week and fix the spacing oversight on the "FREEPLAY" display function, but aside from that all is done!