Game hacking

Gargamel

Paypig
root access
Warning: Nerdpost.

So I got curious about how text is drawn in Asteroids, the arcade game, and how hard it'd be to change some of it. I succeeded, and then I thought I'd write this effortpost to share with you how I did it.

So Asteroids is a vector game. That means there are no graphics in the ROM, unlike most video games of the time which would contain literal bitmap images of everything you see on screen. In Asteroids, the game directly drives the electron gun in the monitor, steering the beam to draw a series of straight lines to show the game visuals. This has the advantage of being relatively high resolution (the vector coordinates range from 0 to 1024 in both X and Y directions, compared to a contemporary game like Pac-Man which runs at 256x224) and the disadvantage that everything on screen is a wireframe with no fill. As well, Asteroids uses a black and white vector monitor, which has the advantage of razor sharp visuals (no jaggies on diagonal lines) and the disadvantage of obviously only being able to display black, white, and shades of gray. Star Wars, another vector game, used a color vector monitor which obviously meant it was in color, at the cost of reduced effective resolution (compared to Asteroids) due to the limitations of the television grade picture tube it used. The effective resolution of Star Wars was limited to the dot pitch of the shadow mask in the picture tube, which roughly equates to 640x480 or perhaps a bit less. Conversely, the effective resolution of Asteroids is limited only by the grain size of the phosphor coating on the inside face of the picture tube, and the monitor's capability to focus the beam, so 1024x1024 or pretty close to it.

Most games of the era (games that aren't vector games) store their graphics data in the form of bitmap images broken up into tiles, and that includes the text seen on screen. The game loads the tiles out of ROM and does whatever is needed to get them on the screen. Pac-Man uses 8x8 tiles displayed on a grid that is 28 tiles wide and 36 tiles tall. The tiles are stored in ROM, which is memory mapped and therefore accessed exactly the same as RAM. The game code loads the tiles out of ROM by reading memory addresses and feeding that data to the video chip, which then draws the tiles on the screen. So if you wanted to change "1UP" to "FUP", "HIGH SCORE" to "BLOWN LOAD" or replacing the cherries for the banana or something like that, it's pretty easy. Since everything on screen is a series of tiles, and all the game is doing is loading tiles out of memory-mapped ROM, all you need to do is tell the program to load some other tiles in place of the ones it would normally load. It's actually a little more complicated than that because of the way color is handled, but I won't get into it here.

Asteroids doesn't have graphics data in ROM like a typical game, so what does it have? Well it actually does have a graphics ROM, except what's contained inside is chunks of code that when called by the game's main program, will take control, draw whatever was requested on the screen as a series of straight point to point lines, and return control to whatever called it.

Now, I don't completelty understand this, but I'll try my best. There is a subroutine to draw each of literally everything seen on screen: the text, the space ship, the projectiles, the asteroids, the flying saucer, and the lives remaining. The letters and numbers in particular are easy to work with because they're indexed similarly to how tiles are arranged in a tile map on a raster game. Most of the phrases, such as "1 COIN 1 PLAY", have a subroutine that feeds index numbers into another subroutine that actually draws the text. This disassembly makes it pretty easy to understand what has to be done.

Code:
;----------------------------------------[ 1 COIN 1 PLAY ]-----------------------------------------

1      1     _     C        O     I     N        _     1     _        P     L     A       Y    NULL  NULL    
2    00011_00001_00111_0, 10011_01101_10010_0, 00001_00011_00001_0, 10100_10000_00101_0 11101_00000_00000_0
3        $18, $4E,            $9B, $64,            $08, $C2,            $A4, $0A,            $E8, $00
So I've tweaked the code a bit to make it easier to understand, but what you have on line 1 is the human readable representation of what the game is going to display, on line 2 is the index of each character expressed in binary and on line 3 are the same values as line 2 but written in hex. Line 3 is what you'd see if you viewed the game's ROM in a hex editor. The characters are encoded as three 5 bit values (15 bits) encoded into two 8 bit bytes (16 bits), with an extra zero bit on the end for padding. So we can see that 00011b gives us a "1", 00001b gives us a whitespace character, 00111 gives us a "C", the zero at the end pads us out to 16 bits even, and that converts to 184Eh. Rinse and repeat for the next six bytes. The last two bytes only contain one letter, "Y" and the rest is all zeros. You have to pad out the remainder of the last two bytes with zeros. This tells the game to draw nothing after the "Y". The underscores in the binary numbers are there to help visualize the seperation between the three 5 bit values and the padding bit. You take them out before converting to hex.

So let's say I want to change this, but I don't know how the fuck to get letters other than the ones shown. Well, somebody was nice enough to do the legwork for us and post it on the Internet as part of the disassembly linked above.
Code:
CharPtrTbl:
L56D4:  .word $CB2C             ;JSR  $5658. SPACE - Index 01.
L56D6:  .word $CADD             ;JSR  $55BA. 0     - Index 02.
L56D8:  .word $CB2E             ;JSR  $565C. 1     - Index 03.
L56DA:  .word $CB32             ;JSR  $5664. 2     - Index 04.
L56DC:  .word $CB3A             ;JSR  $5674. 3     - Not indexed.
L56DE:  .word $CB41             ;JSR  $5682. 4     - Not indexed.
L56E0:  .word $CB48             ;JSR  $5690. 5     - Not indexed.
L56E2:  .word $CB4F             ;JSR  $569E. 6     - Not indexed.
L56E4:  .word $CB56             ;JSR  $56AC. 7     - Not indexed.
L56E6:  .word $CB5B             ;JSR  $56B6. 8     - Not indexed.
L56E8:  .word $CB63             ;JSR  $56C6. 9     - Not indexed.
L56EA:  .word $CA78             ;JSR  $54F0. A     - Index 05.
L56EC:  .word $CA80             ;JSR  $5500. B     - Index 06.
L56EE:  .word $CA8D             ;JSR  $551A. C     - Index 07.
L56F0:  .word $CA93             ;JSR  $5526. D     - Index 08.
L56F2:  .word $CA9B             ;JSR  $5536. E     - Index 09.
L56F4:  .word $CAA3             ;JSR  $5546. F     - Index 10.
L56F6:  .word $CAAA             ;JSR  $5554. G     - Index 11.
L56F8:  .word $CAB3             ;JSR  $5566. H     - Index 12.
L56FA:  .word $CABA             ;JSR  $5574. I     - Index 13.
L56FC:  .word $CAC1             ;JSR  $5582. J     - Index 14.
L56FE:  .word $CAC7             ;JSR  $558E. K     - Index 15.
L5700:  .word $CACD             ;JSR  $559A. L     - Index 16.
L5702:  .word $CAD2             ;JSR  $55A4. M     - Index 17.
L5704:  .word $CAD8             ;JSR  $55B0. N     - Index 18.
L5706:  .word $CADD             ;JSR  $55BA. O     - Index 19.
L5708:  .word $CAE3             ;JSR  $55C6. P     - Index 20.
L570A:  .word $CAEA             ;JSR  $55D4. Q     - Index 21.
L570C:  .word $CAF3             ;JSR  $55E6. R     - Index 22.
L570E:  .word $CAFB             ;JSR  $55F6. S     - Index 23.
L5710:  .word $CB02             ;JSR  $5604. T     - Index 24.
L5712:  .word $CB08             ;JSR  $5610. U     - Index 25.
L5714:  .word $CB0E             ;JSR  $561C. V     - Index 26.
L5716:  .word $CB13             ;JSR  $5626. W     - Index 27.
L5718:  .word $CB1A             ;JSR  $5634. X     - Index 28.
L571A:  .word $CB1F             ;JSR  $563E. Y     - Index 29.
L571C:  .word $CB26             ;JSR  $564C. Z     - Index 30.
For some reason numbers 3 through 9 don't have an index, but I don't care because I'm not using them.

Let's say I want the first three letters of that string to be "INS". I'm going to look up "I" in the index and find it has a value of 13. I'll convert 13 into binary using a calcluator, and the result is 01101b. Moving on to the "N", we find that it's index 18 or 10010b. The "S" is index 23 or 10111b. Add on one more zero for padding, and you get 0110110010101110b. Convert this to hex and you have 6CAEh.

To put these changes in the game, load up the ROM in a hex editor. Raading the disassembled code we know the particular bytes we're looking for live at address 57a5h in the game's memory map, and that this particular ROM starts at address 5000h. So we can subtract 5000h from 57a5h and end up with an offset of 7a5h. We find this in the hex editor and overwrite the existing bytes with our modified values.

We do this for the other 8 bytes to get our desired text, which ends up looking like this:
Code:
  I    N    S       E    R    T       _    S    H       E    K   E        L    S
0110110010101110  0100110110110000  0000110111011000  0100101111010010  1000010111000000
      6CAE             4DB0             0DD8            4BD2              85C0
Notice that my replacement string is one character longer than the original. This is okay because there were two unused characters at the end to begin with, so the space was already allocated in the ROM. It fits. So we now overwrite the next 8 bytes of the ROM with these values, and save it.

When we boot the game, the result is this:
Screenshot_2024-11-09_23-19-48.png
 
Back
Top