You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A reverse-engineering of the 6502 binary image to assembly source code of the classic VIC-20 game JETPAC by ULTIMATE PLAY THE GAME.
JETPAC was one of my favourite games on the VIC-20 in the early '80s, the smooth movement of the sprites and excellent game play mechanics made it a standout title. I wanted to look into the code to see how it all works.
After 200+ commits over 18 months, it's probably as completed as I will make it:
Code/Data/Graphics sections are identified.
Variables are labelled and described.
Memory map is complete.
Zero Page and other variables are identified.
Code labels and commenting is complete.
User-Defined Graphics characters have been identified (Excel spreadsheet).
Debugger - MAME debugger was used for single stepping the code and to create the initial code/data separation using the trackpc instruction. Functionality appears significantly better than that available in VICE without external debuggers (which tend to be only available for Commodore 64).
Disassemblers -
dasmfw was used to disassemble the code, which reads hand-crafted "info" files and uses them to format the binary code into source code files. Single stepping the code in MAME debugger and subsequent creation of the info files was the vast majority of the reverse-engineering effort.
Infiltrator was used to check the code/data separation and provide a list of label references than was then fed into dasmfw for easy reference.
Ghidra Function Graph functionality was used later on in the project to visualize the most complex of routines that weren't practical to understand as linear source code or to manually flowchart on paper. Paint.net was used to manually piece together the graph screenshots.
Automated Source Code Editing - GNU sed was used for automated editing of the disassembled source code as a workaround for dasmfw not (yet) supporting local variables.
Assembler - Dasmfw for 6502 by default produces source code that can be assembled with the Kingswood as65 assembler, which is what was used. Other assemblers, e.g. 64tass, can also be targeted by dasmfw using parameters, but this has not yet been attempted.
Build System - GNU Make was used to sequence the disassembly, post-editing, reassembly and comparison to original binary image. GNU md5sum was used for comparing original and recompiled binary files.
File Comparison - Beyond Compare was used for comparing binary memory dumps for discovering variables, data structures etc.
Other Stuff - Microsoft Excel was used for making lookup tables of memory maps for I/O, screens, colours, User-Defined Graphics and more. Microsoft Word was used for creating the code listing overview picture.
Use dasmfw to disassemble the original binary image and create source code based on instructions in info files that dasmfw reads.
Use sed to add local variable names to Zero Page memory addresses.
Use as65 to reassemble the source code back to a binary image.
Use md5sum to compare the original and newly reassembled binary image to ensure they are identical.
The "info" files mentioned previously are used to tell dasmfw how the source code should be disassembled. They are developed by observing and/or single-stepping the program code running in the MAME debugger and updating the info files with appropriate disassembly commands.
Windows
The makefile in the root of the repository contains all these steps, running make will perform the above steps. All the tools for running the makefile are provided for Windows in the repository, you should be able to run make from the command prompt.
Linux / Mac
I've never tried this, but dasmfw is C++ program designed to be easy to compile for your platform. as65 is available as a Linux version, which may also work on Mac. The other utils are standard Unix tools so are probably available on Linux / Mac by default.
The project originally started off to find out how the smooth sprite graphics worked, I had an idea in my head, but wanted to see if this was indeed how it was done. However, JETPAC was not built on previously existing code, the original Sinclair ZX Spectrum version of the game was completely original and the VIC-20 version was a port of that, so reverse-engineering prior art was somewhat limited.
As it turns out, the graphics engine is not standalone, it's embedded into the rest of the game code, so, inevitably, I ended up reverse a lot more than just a sprite engine. In the end I just decided to do all of it. The point then became how to reveal the secrets of the VIC-20 version of the game, given it was the only ULTIMATE game produced on the VIC-20 and thus is somewhat unique.
This section presents an overview of the complete program and highlights some noteworthy aspects and sections of code as described by the text. Most of the routines are relatively easy to understand by looking directly at the code. However, some of the code routines are sufficiently complex that, to be understood, they need to be flowcharted.
NOTE: Throughout the code and description, I use the word "object" and not "sprite". This is deliberate; whilst the majority of objects in the code are managing graphical elements, the objects also store other attributes, such as position, state, direction etc.
NOTE2: Most of the sub-headings in the README link directly to the relevant point in the source code.
NOTE3: Throughout the code, most objects are described as having have X and Y Position and Direction. In some cases Direction should mean Velocity, as the parameter describes not just the direction that the object is moving, but also the rate it is moving i.e. the velocity.
Source Code Map
The complete binary disassembly process generates a single source code file, to which I have added large-font banners to all the important routines so they can be seen from a source code map, shown below, to get an overall 'feel' for the code. Because the banners are all in capital letters, the object handler routine banners that are called from Main Loop have horizontal lines top and bottom e.g. for JETMAN_FLYING.
Ghidra SRE
To provide flowcharts, after initially drawing everything out on paper, I used the Ghidra software reverse-engineering (SRE) tools. Whilst Ghidra is really geared towards compiled high-level programs on modern hardware e.g. Intel x86, good results for 8-bit assembly programs are also possible, but you have to work at it.
The diagrams presented here were created at the end of the reverse-engineering process and so address labels had to be loaded into Ghidra using a script to get any from of usable flowchart (or Function Graph in Ghidra terms).
Additionally, Ghidra is somewhat automated regarding separation of code subroutines, you can't control it (without changing the code) and having assembly subroutines called from a jump table doesn't help, so the flowcharts may be incomplete, have missing comments/value names and should be viewed together with the reverse-engineered source code listing, where commentary etc. is much richer.
Ghidra is an interactive tool and lets you analyze the code is a much more visual and interactive way than raw assembly source code, if you have some time to get a handle on it, it can provide a level of insight that just isn't possible with linear code analysis.
Source Code Listing
The source code can be, roughly, viewed as three parts, plus graphics data:
Program Initialize / Game Select / Main Loop
Object and Handlers
General routines e.g. reading controls, drawing text & objects to the screen
JETPAC requires an 8kb memory expansion and, once the program has been loaded into memory between $2000 and $3FFF, execution starts at $201D, where it sets up the interrupt handler vectors, erases
variable memory, sets-up the VIA I/O ports and configures the VIC chip.
The VIC chip configures the Screen RAM, Used-Defined Graphics RAM and Colour RAM mapping parameters to 11 rows by 23 columns, with each character being 16x8 pixels wide by 16 pixels high, and in this mode, colour tiles are also 16x8 pixels.
The game select screen flashes the chosen options, by inverting the character-set bitmaps already drawn onto the screen on a continual basis until start game is selected. Rather than simply replacing each whole character for an inverted version, it inverts the whole bar of screen text byte-by-byte, like all the other in-game graphics. The keyboard is read to select game options and the Spacebar starts the game.
There are a maximum of four laser objects used at any one time. The initialization routine works out where Jetman is on-screen and the direction he faces, whether the new laser being created will screen-wrap, the vertical height of Jetman's gun and then creates an object with these parameters, together with a random length and colour from a colour table, using the IRQ timer.
Main Loop gets next active object type from the object table and, using an indexed jump table, uses it to jump to the appropriate object handler. When the processing for each object has completed, GOTO_NEXT_OBJECT is called.
Simply put, the essence of JETPAC is up to 15 objects, (14 graphical + 1 sound), each with a set of properties and methods, displayed moving on-screen, with the bare minimum of background static objects i.e. the platforms and scores.
Thus, the object table and object handlers are the core of the game. The following pictures show a screenshot of JETPAC during play and a memory dump of the corresponding object table with some commentary for the objects.
Byte 00 in each object's 8-byte record denotes object type, bytes 01-04 typically X position & direction, Y position & direction, bytes 05-07 various other parameters e.g. colour.
Using Ghidra Function Graph view, we can map of all the object handlers called from Main Loop in one picture.
Note, this picture is just to give a feel for the code, you can zoom in, but not so you can read the text comfortably. To look at all the flowcharts, you need to open the project in Ghidra for yourself.
The most complex routine in the whole program is what gives JETPAC it's super-smooth responsiveness, with player-controlled acceleration/deceleration and gravity effects tuned to perfection.
A summary of the flowcharted routine:
Note that where it shows ZP_Laser_Param_Countdown, it's a bad local variable name replace, it should say ZP02_Collision_Status🤦♂️.
On entry, Jetman is tested for a platform collision, if yes, the code top-right from $3282 on the graph works out what collision has happened depending on Jetman's current position and direction, ranging from transitioning from flying to standing to reversing direction and several events in between.
At the end of the collision testing and, together with the Direction X state, the new X-axis Position and Direction have been calculated and the code will store the Direction then the Position or just the Position only, bypassing the Direction.
Storing X Position is the box in the centre of the flowchart that everything must pass through, except the path to the left when Jetman transitions from Flying to Standing. Additionally, when storing the new Position X, left or right screen wraparound is accounted-for.
Now the Y collision is assessed and the new Y-axis Position and Direction are calculated, which is bit simpler than X due to Jetman's thrust only operating up/left/right, he doesn't bounce off platforms going downwards and there being no Y-axis wraparound.
At the bottom of the flowchart, Jetman Y-axis Direction and Position are updated, or, like for X-axis, just the Position only. This is followed by code shared with JETMAN_STANDING for displaying Jetman in his new position and, if fire had been pressed, a new laser object may be initiated, before calling GOTO_NEXT_OBJECT.
The source code I have produced so far for this routine is still sparsely commented (though the labels tell much of the story). A lot of debugger single-stepping and paper flowcharts are what motivated me to move to Ghidra Function Graphing. The new flowchart is a huge improvement and the source code could now be improved much further.
Similar to JETMAN FLYING, but without the Platform Collision tests.
A summary of the flowcharted routine:
The first thing the routine does is to check if the flashing score countdown his finished, when you start a wave, the score flashes for a few seconds and Jetman is not drawn until the countdown reaches zero.
One flashing score is complete, Jetman is tested to see if his height has increased, if yes, he transitions from Standing to Flying and most of the code is bypassed using the path on the right hand side of the flowchart.
If Jetman is not Standing, his X Direction is tested to see if has changed and, if not, flow passes through the upper middle/left code that prepares for setting Direction X to 1 or -1 (i.e $FF). If Jetman has changed direction, Jetman's Status and Direction parameters are set instead.
Like for JETMAN_FLYING, storing X Position (and Direction) is the box in the centre of the flowchart that everything must pass through, except the path to the left when Jetman transitions from Standing to Flying. Additionally, when storing the new Position X, left or right screen wraparound is accounted-for.
This is followed by code shared with JETMAN_FLYING for displaying Jetman in his new position and, if fire had been pressed, a new laser object may be initiated, before calling GOTO_NEXT_OBJECT.
When an object explodes, e.g. an alien is hit by a laser, it's object type is changed to an explosion and subsequent calls to this routine will animate the explosion though a list of explosion graphics from a data table and then ending.
Explosions are set to random colours, excluding green, which is used only for platforms.
The objects on-screen for Ship Modules, Fuel Cells and Valuables share some of the same object list locations, so this routine has some calculations and masks to manage the object parameters accordingly.
Alien Waves
The eight alien waves and four rocket ships of the original Spectrum version were reduced to only four and two respectively for the VIC-20 version of the game, probably due to memory constraints.
Each alien on each wave has it's own object in the object table and is handled separately.
WAVE 0 FUZZBALL aliens are simply objects that float across the screen with a randomly generated trajectory, either parallel to the planet surface or in a shallow downwards trajectory, and any contact with Jetman, a platform or the ground will cause them to explode.
The first test in the code, common to all waves, is whether the alien has been hit by a laser, if it has, the common score routine calculates the increased score based on the wave and the object and changes it to an explosion object.
Otherwise, the alien is tested to see if it has collided with a platform, which, for Wave 0, this causes it to change into an explosion object, else it's new position on-screen is stored to it's object record and it is redrawn.
'WAVE 1 CROSS` is similar to Wave 0, except the alien graphic is different and collisions with platform result in the alien bouncing off in a different direction.
WAVE 2 SPHERE is similar to Wave 1, except the alien graphic is different and the alien will also change direction occasionally, based a pseudo-random number calculated using the IRQ and Raster interrupt values.
WAVE 3 SAUCER is similar to Wave 2, except the alien graphic is different and the alien direction is dictated by the position of Jetman - they home in on Jetman.
When the ascend/descend objects are triggered at end of wave, Jetman is removed from the screen and the ship goes up and comes back down again to the next Wave.
Because it will not fit on the screen until the ship is a several lines off the bottom, the rocket flame graphic has a delay before it is displayed.
Sounds are processed by the object handler, just like the graphics display objects. Data entered into the sound object by other object handlers is used to index into a jump table that the sound object uses to process the various sound effect routines.
The sounds are relatively simple and don't take up much memory or CPU cycles, but are effective.
This subroutine is called from main loop and is used to update one of the lasers on display.
Once a laser has been fired, over several animation frames, it increases in length to a predetermined size, then decays in four sections by having bits erased from the screen i.e. it starts off as a solid line then becomes dashed, then dotted and eventually disappears completely.
The patterns of dots are predetermined from a data table and current decay state is stored in the object record itself.
A summary of the flowcharted routine:
The DISPLAY_LASERS code is in several parts, linked using JSR/RTS, which Ghidra renders as separate graphs. The Laser_Wrap routine, bottom right, is called from $21CB on the left. The purple box around some of the code can be ignored, it is a warning produced by Ghidra that, when seeing the Load_ZP_Parameters routine that is JSR'd to, it can't match the return address, because it is manipulated in the Load_ZP... routine.
The code on the left of the diagram handles creation of the initial laser beam on-screen, stretching from Jetman's gun the code at $21E1 is where the laser beam is drawn onto the screen using an XOR (i.e. EOR).
The code in the purple box runs when the laser is fully drawn on screen with a solid line, it then depletes the solid lines using some bit-patterns copied to the laser beam object, which are iteratively cascaded, eventually erasing the laser beam completely over several calls.
Note there are several routine exit points in the purple box, those denoted JMP switchD_2617, which returns to GOTO_NEXT_OBJECT.
General routines e.g. reading controls, drawing text & objects to the screen
This utility routine is used to load multiple 16-bit static data values, e.g. addresses, into one or more Zero Page variables.
The routine is called from 30+ places in the code, more than any other routine.
The data to be loaded is assembled directly after the call to the routine, which then utilizes the return address to get the data, incrementing the eventual return address as it goes along.
When $FF is encountered, the routine returns to continue execution after the call and data.
Once the timer has triggered an IRQ interrupt, this routine will stash the next object to be handled, set Jetman to be updated next, then restores the stashed next object, which serves to regulate Jetman's movement update speed.
These routines handle creation of new objects depending on game parameters, e.g. whether Jetman is on-screen, whether all Ship Parts and Fuel Cells have been collected.
The position of the new object is semi-randomly chosen from look-up table of X-axis start positions.
This routine is used for testing all on-screen objects whether they have collided with a platform e.g. objects falling from the top of the screen, Jetman or aliens flying in any direction.
The collision testing sets bits in a return status byte that is then tested by the calling routine to see whether the collision is of relevance.
A summary of the flowcharted routine:
The routine is run for a single object in the object list at a time e.g. for one alien object, and the routine includes a loop iteration for each of the three platforms.
The entry point to the routine is the top-leftmost code at $30b3, which loads the object X and Y values into zero page variables, then tests to see if the object is to the left of the current platform being tested. Platform co-ordinates and width are loaded from small lookup table.
If the object is to the left of the platform, then further tests are made to see by how much, given the object bitmap has a width and the X and Y position of the object pertain to the bottom-left of the object.
If the object is to right of the leftmost edge of the platform, then the large block of code at $309B is run, which looks up the width of the platform and calculates the distance of the object from the rightmost edge of the current platform, then uses the same test as the left side.
If the object is not vertically overlapping a platform, the code $3119 is run, which iterates to the next platform for the current object, otherwise the object is overlapping and thus position Y must be tested.
The code in the bottom right hand corner tests to see if the object position Y causes it to overlap a platform. Note there are multiple exits using JMP Set_Collision_Status, which is JSR'd to in other places, but when called with a JMP, uses the JSR to exit the routine for the object.
Once the object has been tested for collision with the platforms, it is then tested to see if the object has hit the ground and a collision status bit set accordingly and the routine exited.
The reverse-engineered source code is reasonably well commented, but the exact meaning of each bit of the collision status byte is not documented, this could be improved upon.
Depending on user-selected game options, joystick or keyboard controls are read, with both being read through the VIA I/O interface chips.
The keyboard control is quite novel in the way it allows several different keys to be used to play the game, which are grouped based on the hardware keyboard matrix.
Due to the way that the game code configures the VIC chip to be a memory-mapped display, this and a few other routines are needed to convert object X-Y positions to actual memory addresses for object for bitmap and colour display purposes.
This routine utilizes a pre-calculated address look-up table to speed up the process.
This routine is probably where the game spends the majority of processing time.
Usually, the object to be displayed will be moving, so it is given two sets of parameters, one for the objects current position and another for the new position.
Remember that objects are drawn on the screen by simple XOR textures i.e.XOR'ing what's there already, which allows on-screen objects to overlap with minimal processing complications.
A summary of the flowcharted routine:
The routine will firstly compare the positions of the objects and then erase the lines of the old object that will not be replaced e.g. if the object is moving upwards, you can remove some of the bottom lines from it. This is the circle of routines in the top-right of the flowchart starting $3661.
The Erase_Old_Object at $3688 through which all flows must pass, together with three code chunks directly connected to it on the right and downwards, erase the old object animation frame and copy in the new frame directly on top a byte-by-byte. Note the objects are drawn bottom to top, due to the use of a decremented index Y register loop used to read/write the graphics data to the screen.
The code below $36AF is concerned with moving to the next column of screen data to be erased and written to again with new object frame data.
Updates the colour map tiles based on the X-Y position of the object it's given, probably works the same way as Sinclair Spectrum games and thus gives you the same colour clash characteristic.
The routine checks to ensure it's not changing the green colour map tiles of the platforms.
; Colorize all object sprites i.e. Jetman, Aliens, Ship Top/Middle/Base, Fuel, Valuables
; L_JSR_($381F)_($2961) OK
; L_JSR_($381F)_($2CC6) OK
; L_JSR_($381F)_($2FD4) OK
; L_JSR_($381F)_($32FC) OK
; L_JSR_($381F)_($335B) OK
Colourize_Object
lda ZP18_Object_Position_X ; 381F: A5 18
sta ZP04 ; 3821: 85 04
lda ZP19_Object_Position_Y ; 3823: A5 19
sta ZP05 ; 3825: 85 05
; Setup Colour RAM params based on object position X & Y via ZP04/ZP05 e.g. Ship Top X=20, Y=3F
jsr Setup_Colour_RAM_Address ; 3827: 20 F7 34
; Divide object height by $10 then add $02, so always update colour on at least two vertical tiles
; e.g. Jetman=$18->$03, Ship_Top=$10->$03, Fuzz_Alien=$0A->$02
lda ZP17_Object_Size_Y_Pixels ; 382A: A5 17
lsr a ; 382C: 4A
lsr a ; 382D: 4A
lsr a ; 382E: 4A
lsr a ; 382F: 4A
clc ; 3830: 18
adc #$02 ; 3831: 69 02
; Use ZP0B as outer loop for vertical tile rows
sta ZP0B_Colour_RAM_Tiles_Y ; 3833: 85 0B
; Use ZP0A as inner loop for horizontal tile columns
lda ZP16_Object_Size_X_Columns ; 3835: A5 16
sta ZP0A_Colour_RAM_Tiles_X ; 3837: 85 0A
inc ZP0A_Colour_RAM_Tiles_X ; 3839: E6 0A
; L_BRS_($383B)_($385C) OK
Loop_Y
ldx ZP0A_Colour_RAM_Tiles_X ; 383B: A6 0A
ldy #$00 ; 383D: A0 00
; Read colour from RAM and if it's green i.e. colour of the platforms, skip the colour change
; else write the updated colour to the colour RAM
; L_BRS_($383F)_($384D) OK
Loop_X
lda (ZP0C_Colour_RAM_Tile_Addr_Lo),y; 383F: B1 0C
and #%00000111 ; 3841: 29 07
cmp #COLOUR_GREEN ; 3843: C9 05
beq Green_Ignored ; 3845: F0 04
lda ZP1B ; 3847: A5 1B
sta (ZP0C_Colour_RAM_Tile_Addr_Lo),y; 3849: 91 0C
; L_BRS_($384B)_($3845) OK
Green_Ignored
iny ; 384B: C8
dex ; 384C: CA
bne Loop_X ; 384D: D0 F0
; Move to next row to colour (i.e. move up) and test for going out of
; screen boundry at top of screen, early exit if so
lda ZP0C_Colour_RAM_Tile_Addr_Lo ; 384F: A5 0C
sec ; 3851: 38
sbc #SCREEN_WIDTH_COLUMNS ; 3852: E9 17
cmp #SCREEN_WIDTH_COLUMNS ; 3854: C9 17
bcc Colourize_Object_RTS ; 3856: 90 06
; Update the Colour RAM for the appropriate tile, move up one row
sta ZP0C_Colour_RAM_Tile_Addr_Lo ; 3858: 85 0C
dec ZP0B_Colour_RAM_Tiles_Y ; 385A: C6 0B
bne Loop_Y ; 385C: D0 DD
; L_BRS_($385E)_($3856) OK
Colourize_Object_RTS
rts ; 385E: 60
The below table shows the Colour RAM, after a few seconds of Wave 0 Fuzzball (re-)start. The Colour RAM is initialised with $01 (=white) and I have replaced all $01s with ".." in the table below for clarity.
As the alien objects float onto the screen from the sides, they leave an invisible trace in the Colour RAM ($06=Blue and $02=Red). Platforms are where you'd expect them to be ($05=Green) and the High Score is coloured at top-centre ($07=yellow).
The first lookup table is for Jetman and the alien graphics for each of the four waves and they are characterized by being having animated horizontal movement during the game.
Each UDG frame data ends with some values describing e.g. the number of lines the object data contains and width in columns, but note the data is read from the high-byte downwards for efficient CPU-cycle loops.
Jetman has four frames of animation for standing/walking or flying in each direction, the alien graphics have two frames each.
The second block of graphics data has a separate lookup table with addresses for the rocket ship, fuel cell and valuables.
Additionally, graphics data is also present for flames, explosions, fuel cell and platforms, but they are addressed directly, not via any lookup table.