Skip to content

A reverse-engineering of the classic VIC-20 game, JETPAC, by ULTIMATE PLAY THE GAME.

Notifications You must be signed in to change notification settings

phillipeaton/JETPAC_VIC-20_disassembly

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

VIC-20 JETPAC Disassembly and Reverse-Engineering

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).

This repository contains:

  1. Tools Used

  2. Repository Contents

  3. Disassembly and Reassembly Instructions

  4. Source Code Highlights

Tools Used

  • 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.

Back to top

Repository Contents

See also the further readme files in the lower level directories of the repository.

  • bin_orig - Original program binary image file that gets disassembled.

  • dasmfw - Info files to control the disassembly, sed script file, source code gets built here.

  • bin - New program binary image file that gets reassembled.

  • Infiltrator - Files used to confirm data/code separation and create source code cross-reference comments.

  • MAME_debug - Files to run MAME debugger, config settings, VIC-20 ROMs, JETPAC .prg file.

  • utils - make and programs called by make.

  • ghidra - A Ghidra project file that was used to visualize the code using Function Graphs.

  • docs - Notes and technical documentation of the VIC-20 and the JETPAC program.

Back to top

Disassembly and Reassembly Instructions

The basic approach is:

  1. Use dasmfw to disassemble the original binary image and create source code based on instructions in info files that dasmfw reads.

  2. Use sed to add local variable names to Zero Page memory addresses.

  3. Use as65 to reassemble the source code back to a binary image.

  4. 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.

Back to top

Source Code Highlights

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.

JETPAC Source One-pager

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:

  1. Program Initialize / Game Select / Main Loop

  2. Object and Handlers

  3. General routines e.g. reading controls, drawing text & objects to the screen

  4. User-Defined Graphics data

Program Initialize / Game Select / Main Loop

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.

; Screen Map RAM Location
; -----------------------
; VIC_R2_Num_Cols Bit 7 (A=1) and VIC_R5_CM_Start Bits 7654 (BBBB=1000=$8) form part of a
; a 14-bit address that signifies the start of the Screen Map RAM: BB_BBA0_0000_0000
; Using the values in the Init Table: 10_0010_0000_0000 NOTE: R5 bit 7 is inverted by the VIC
; Thus Screen Map RAM starts at $0200
; Colour Map RAM Location
; -----------------------
; VIC_R2_Num_Cols Bit 7 (A=1) signifies Colour Map RAM starts at $9600
; Character Map RAM Location
; --------------------------
; VIC_R5_CM_Start Bits 4321 (CCCC = 1100 = $C) signifies Character Map RAM starts at $1000
; Screen Size Rows x Columns
; --------------------------
; VIC_R2_Num_Cols Bits 6543210 set number of columns = _001_0111 = $17 = 23 decimal
; VIC_R3_Num_Rows Bits 654321 set number of rows = _001_011_ = $0B = 11 decimal
; NOTE: VIC_R3_Num_Rows Bit 0=1 sets character matrices to 8x16 pixels
VIC_Init_Table
fcb $0B ; 2004: 0B VIC_R0_H_Ctr Left edge of TV picture and interlace switch.Left edge of TV picture and interlace switch.
fcb $26 ; 2005: 26 VIC_R1_V_Ctr Vertical TV picture origin.
fcb $97 ; 2006: 97 VIC_R2_Num_Cols Number of columns displayed, part of screen map address.
fcb $17 ; 2007: 17 VIC_R3_Num_Rows Number of character lines displayed, part of raster location.
fcb $00 ; 2008: 00 VIC_R4_TV_Raster Raster beam location bits 8-1.
fcb $8C ; 2009: 8C VIC_R5_CM_Start Screen map and character map addresses.
fcb $00 ; 200A: 00 VIC_R6_H_LightP Light pen horizontal screen location.
fcb $00 ; 200B: 00 VIC_R7_V_LightP Light pen vertical screen location.
fcb $00 ; 200C: 00 VIC_R8_Paddle_X Potentiometer X/Paddle X value.
fcb $00 ; 200D: 00 VIC_R9_Paddle_Y Potentiometer Y/Paddle Y value.
fcb $00 ; 200E: 00 VIC_RA_Frq_Osc1 Relative frequency of sound oscillator 1 (bass).
fcb $00 ; 200F: 00 VIC_RB_Frq_Osc2 Relative frequency of sound oscillator 2 (alto).
fcb $00 ; 2010: 00 VIC_RC_Frq_Osc3 Relative frequency of sound oscillator 3 (soprano).
fcb $00 ; 2011: 00 VIC_RD_Frq_Noise Relative frequency of sound oscillator 4 (noise).
fcb $00 ; 2012: 00 VIC_RE_Vol_Color Sound volume and auxiliary color.
fcb $08 ; 2013: 08 VIC_RF_BckBdrCol Background color, border color, inverse color switch.

; Reset the VIC registers
ldx #$0F ; 2056: A2 0F 16 registers
; L_BRS_($2058)_($205F) OK
Reset_VIC
lda VIC_Init_Table,x ; 2058: BD 04 20
sta VIC_R0_H_Ctr,x ; 205B: 9D 00 90
dex ; 205E: CA
bpl Reset_VIC ; 205F: 10 F7

; Initialize Screen RAM i.e. map screen tiles to User-Defined Graphics RAM
; Each character is a 16x8 tile i.e. double height, thus play area
; is decimal 11 Rows x 23 Columns
; 0200 00 0B 16 21 2C 37 42 4D 58 63 6E 79 84 8F 9A A5 B0 BB C6 D1 DC E7 F2
; 0217 01 0C 17 22 2D 38 43 4E 59 64 6F 7A 85 90 9B A6 B1 BC C7 D2 DD E8 F3
; 022E 02 0D 18 23 2E 39 44 4F 5A 65 70 7B 86 91 9C A7 B2 BD C8 D3 DE E9 F4
; 0245 03 0E 19 24 2F 3A 45 50 5B 66 71 7C 87 92 9D A8 B3 BE C9 D4 DF EA F5
; 025C 04 0F 1A 25 30 3B 46 51 5C 67 72 7D 88 93 9E A9 B4 BF CA D5 E0 EB F6
; 0273 05 10 1B 26 31 3C 47 52 5D 68 73 7E 89 94 9F AA B5 C0 CB D6 E1 EC F7
; 028A 06 11 1C 27 32 3D 48 53 5E 69 74 7F 8A 95 A0 AB B6 C1 CC D7 E2 ED F8
; 02A1 07 12 1D 28 33 3E 49 54 5F 6A 75 80 8B 96 A1 AC B7 C2 CD D8 E3 EE F9
; 02B8 08 13 1E 29 34 3F 4A 55 60 6B 76 81 8C 97 A2 AD B8 C3 CE D9 E4 EF FA
; 02CF 09 14 1F 2A 35 40 4B 56 61 6C 77 82 8D 98 A3 AE B9 C4 CF DA E5 F0 FB
; 02E6 0A 15 20 2B 36 41 4C 57 62 6D 78 83 8E 99 A4 AF BA C5 D0 DB E6 F1 FC
ldx #$00 ; 2061: A2 00
txa ; 2063: 8A
; Initialise Screen RAM outer loop
; L_BRS_($2064)_($2077) OK
ISR_Columns
ldy #SCREEN_COLUMNS ; 2064: A0 17
pha ; 2066: 48
; Initialise Screen RAM inner loop
; L_BRS_($2067)_($206F) OK
ISR_Rows
sta Screen_RAM,x ; 2067: 9D 00 02
clc ; 206A: 18
adc #SCREEN_ROWS ; 206B: 69 0B
inx ; 206D: E8
dey ; 206E: 88
bne ISR_Rows ; 206F: D0 F6
pla ; 2071: 68
clc ; 2072: 18
adc #$01 ; 2073: 69 01
cmp #SCREEN_ROWS ; 2075: C9 0B
bne ISR_Columns ; 2077: D0 EB

JETPAC Memory Map

JETPAC Memory Map

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.

; 88 88b 88 88 888888 88 db .dP"Y8 888888 88""Yb
; 88 88Yb88 88 88 88 dPYb `Ybo." 88__ 88__dP
; 88 88 Y88 88 88 88 .o dP__Yb o.`Y8b 88"" 88"Yb
; 88 88 Y8 88 88 88ood8 dP""""Yb 8bodP' 888888 88 Yb
; dP"Yb 88""Yb 88888 888888 dP""b8 888888 .dP"Y8
; dP Yb 88__dP 88 88__ dP `" 88 `Ybo."
; Yb dP 88""Yb o. 88 88"" Yb 88 o.`Y8b
; YbodP 88oodP "bodP' 888888 YboodP 88 8bodP'
; Initialise laser objects once every four calls based on IRQ counter if fire pressed
; L_JSR_($20E9)_($3306) OK
Init_Laser_Objects
lda ZP_IRQ_Counter_Lo ; 20E9: A5 4A
and #$03 ; 20EB: 29 03
beq Do_Init_Laser_Objects ; 20ED: F0 01
rts ; 20EF: 60
; ==========================================
; L_BRS_($20F0)_($20ED) OK
Do_Init_Laser_Objects
jsr Load_ZP_Parameters ; 20F0: 20 9D 25
fcb $06 ; 20F3: 06 ZP06/07
fdb Obj_Laser_0 ; 20F4: 88 03
fcb $FF ; 20F6: FF
; Loop through the four possible Laser shots, branch if any are zero, i.e. first shot setup
ldx #MAX_LASERS ; 20F7: A2 04
ldy #LASER_TYPE_PARAM ; 20F9: A0 00
; L_BRS_($20FB)_($210D) OK
Loop_Update_Laser_Objects
lda (ZP06_Laser_Old_State_Lo),y ; 20FB: B1 06
beq Init_Laser_Object ; 20FD: F0 11
; Move 16-bit address pointer to next laser shot object i.e. 8 bytes ahead
lda ZP06_Laser_Old_State_Lo ; 20FF: A5 06
clc ; 2101: 18
adc #$08 ; 2102: 69 08
sta ZP06_Laser_Old_State_Lo ; 2104: 85 06
lda ZP07_Laser_Old_State_Hi ; 2106: A5 07
adc #$00 ; 2108: 69 00
sta ZP07_Laser_Old_State_Hi ; 210A: 85 07
; Test if all Laser objects updated, if yes, return, else loop back
dex ; 210C: CA
bne Loop_Update_Laser_Objects ; 210D: D0 EC
rts ; 210F: 60

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.

; 8b d8 db 88 88b 88 88 dP"Yb dP"Yb 88""Yb
; 88b d88 dPYb 88 88Yb88 88 dP Yb dP Yb 88__dP
; 88YbdP88 dP__Yb 88 88 Y88 88 .o Yb dP Yb dP 88"""
; 88 YY 88 dP""""Yb 88 88 Y8 88ood8 YbodP YbodP 88
; L_JMP_($25FA)_($25DD) OK
; L_JMP_($25FA)_($265B) OK
Main_Loop_Init
lda #$00 ; 25FA: A9 00
sta ZP_Alien_Spawn_Counter ; 25FC: 85 21
; Reset object list index to $03B0 i.e. Ship Ascend/Decend
lda #$B0 ; 25FE: A9 B0
sta ZP_Obj_List_Ptr_Lo ; 2600: 85 00
lda #$03 ; 2602: A9 03
sta ZP_Obj_List_Ptr_Hi ; 2604: 85 01
; Enable Interrupts (Clear Interrupt Disable)
cli ; 2606: 58
; L_BRS_($2607)_($2643) OK
; L_JMP_($2607)_($267D) OK
Main_Loop
ldy #$00 ; 2607: A0 00
; Load object Type into A from object table, multiply by two for address jump table index use
lda (ZP_Obj_List_Ptr_Lo),y ; 2609: B1 00
asl a ; 260B: 0A
; Setup subroutine address from jump table + index and indirect jmp to it
tay ; 260C: A8
lda Object_Handler_Jump_Table,y ; 260D: B9 8C 26
sta ZP_Subroutine_Addr_Lo ; 2610: 85 5F
lda Object_Handler_Jump_Table+1,y ; 2612: B9 8D 26
sta ZP_Subroutine_Addr_Hi ; 2615: 85 60
jmp (ZP_Subroutine_Addr_Lo) ; 2617: 6C 5F 00

Objects and Handlers

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.

JETPAC Gameplay Objects JETPAC Object Table

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.

Addr  00 01 02 03 04 05 06 07 ASCII       Object Description
---------------------------------------------------------------------------------------------------
0380  81 25 F8 6C FC 01 00 00   .%ølü...  Jetman facing left, flying, white
0388  00 9B A1 A5 A5 A5 07 01   ..¡¥¥¥..  Laser shot not active
0390  90 93 A1 A5 A5 15 07 02   ..¡¥¥...  Laser shot right active, red
0398  90 8B A1 A5 0D 45 04 03   ..¡¥.E..  Laser shot right active, cyan (recoloured by next laser)
03A0  90 83 A1 AD 1D 41 01 07   ..¡-.A..  Laser shot right active, yellow
03A8  0C 00 00 00 00 00 00 00   ........  Sound currently not being played
03B0  09 70 02 AF 03 00 00 00    p.ÂŻ....  Ship base module, 2 of 6 fuel cells, Base+Mid+Top modules
03B8  04 98 00 2B 01 04 18 18   ...+....  Fuel Cell, purple
03C0  0E 18 00 3E 00 01 28 18   ...>..(.  Valuable, gemstone (multicolour cycle)
03C8  06 60 FB A8 FD 04 03 00   .`û¨ý...  Wave 3 Saucer, purple
03D0  06 79 FB 6F 04 04 03 00   .yûo....  Wave 3 Saucer, purple
03D8  06 6E FB 28 04 04 03 00   .nĂ»(....  Wave 3 Saucer, purple
03E0  03 7D 00 A6 02 07 03 00   .}.¦....  Explosion, yellow
03E8  06 69 FB A1 FB 04 03 00   .iû¡û...  Wave 3 Saucer, purple
03F0  06 63 FB A1 FB 04 03 00   .cû¡û...  Wave 3 Saucer, purple

; dP"Yb 88""Yb 88888 888888 dP""b8 888888
; dP Yb 88__dP 88 88__ dP `" 88
; Yb dP 88""Yb o. 88 88"" Yb 88
; YbodP 88oodP "bodP' 888888 YboodP 88
; 88 88 db 88b 88 8888b. 88 888888 88""Yb .dP"Y8
; 88 88 dPYb 88Yb88 8I Yb 88 88__ 88__dP `Ybo."
; 888888 dP__Yb 88 Y88 8I dY 88 .o 88"" 88"Yb o.`Y8b
; 88 88 dP""""Yb 88 Y8 8888Y" 88ood8 888888 88 Yb 8bodP'
Object_Handler_Jump_Table
fdb GOTO_NEXT_OBJECT ; 268C: 1A 26 00
fdb JETMAN_FLYING_ ; 268E: 73 32 Obj 01
fdb JETMAN_STANDING_ ; 2690: 4D 33 Obj 02
fdb ANIMATE_EXPLOSIONS ; 2692: 34 29 Obj 03
fdb SHIP_PART_OR_FUEL ; 2694: 8E 2F Obj 04
fdb WAVE_0_FUZZBALL ; 2696: 6A 2C Obj 05
fdb WAVE_3_SAUCER ; 2698: BC 2A Obj 06
fdb WAVE_2_SPHERE ; 269A: 6D 2A Obj 07
fdb WAVE_1_CROSS ; 269C: 5F 2A Obj 08
fdb SHIP_BASE_MODULE ; 269E: 74 2E Obj 09
fdb SHIP_ASCEND ; 26A0: 17 2E Obj 0A
fdb SHIP_DESCEND ; 26A2: 4E 2E Obj 0B
fdb SOUND_UPDATE ; 26A4: CD 26 Obj 0C
fdb GOTO_NEXT_OBJECT ; 26A6: 1A 26 Obj 0D
fdb VALUABLES ; 26A8: 3E 28 Obj 0E
fdb GOTO_NEXT_OBJECT ; 26AA: 1A 26 Obj 0F
fdb DISPLAY_LASERS ; 26AC: 9A 21 Obj 10

Using Ghidra Function Graph view, we can map of all the object handlers called from Main Loop in one picture.

JETPAC Object Handler Overview

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.

GOTO NEXT OBJECT prioritizes object cases for Jetman, laser beams and sound objects, plus initiates spawning of new alien objects as needed.

In the code, object handlers called directly by Main Loop are written in uppercase e.g. DISPLAY_LASERS.

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.

JETPAC Object Handler JETMAN FLYING

A summary of the flowcharted routine:

  1. Note that where it shows ZP_Laser_Param_Countdown, it's a bad local variable name replace, it should say ZP02_Collision_Status🤦‍♂️.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

JETPAC Object Handler JETMAN STANDING

A summary of the flowcharted routine:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

; ############################################################################################
; Yb dP db Yb dP 888888 dP"Yb
; Yb db dP dPYb Yb dP 88__ dP Yb
; YbdPYbdP dP__Yb YbdP 88"" Yb dP
; YP YP dP""""Yb YP 888888 YbodP
; 888888 88 88 8888P 8888P 88""Yb db 88 88
; 88__ 88 88 dP dP 88__dP dPYb 88 88
; 88"" Y8 8P dP dP 88""Yb dP__Yb 88 .o 88 .o
; 88 `YbodP' d8888 d8888 88oodP dP""""Yb 88ood8 88ood8
; ############################################################################################
WAVE_0_FUZZBALL
jsr Test_Laser_Hits ; 2C6A: 20 5F 2C
bmi Alien_Hit_Score_Update ; 2C6D: 30 09
; Alien not hit by laser, test if alien hit platform, if yes explode,
; else test if alien hit Jetman
jsr Test_Platform_Collision ; 2C6F: 20 B3 30
lda ZP02_Collision_Status ; 2C72: A5 02
bpl Display_Object_Position_Update ; 2C74: 10 1C
bmi Chg_Obj_To_Explosion_Then_Next ; 2C76: 30 14
; Yb dP db Yb dP 888888 dP""b8 dP"Yb 8b d8 8b d8 dP"Yb 88b 88
; Yb db dP dPYb Yb dP 88__ dP `" dP Yb 88b d88 88b d88 dP Yb 88Yb88
; YbdPYbdP dP__Yb YbdP 88"" Yb Yb dP 88YbdP88 88YbdP88 Yb dP 88 Y88
; YP YP dP""""Yb YP 888888 YboodP YbodP 88 YY 88 88 YY 88 YbodP 88 Y8
; Alien has been hit by laser, use current player wave to create points table offset
; L_JMP_($2C78)_($2A64) OK
; L_JMP_($2C78)_($2A72) OK
; L_JMP_($2C78)_($2AC1) OK
; L_BRS_($2C78)_($2C6D) OK
; L_JMP_($2C78)_($2C9C) OK
Alien_Hit_Score_Update
lda ZP_Wave_Active_Player ; 2C78: A5 59
and #%00000011 ; 2C7A: 29 03
asl a ; 2C7C: 0A
tax ; 2C7D: AA
; Get score high byte and save
lda Alien_Points_Lookup_Table,x ; 2C7E: BD DB 2C
sta ZP02_Score_Byte_Hi ; 2C81: 85 02
inx ; 2C83: E8
; Get score low byte and save
lda Alien_Points_Lookup_Table,x ; 2C84: BD DB 2C
sta ZP03_Score_Byte_Lo ; 2C87: 85 03
jsr Update_Score ; 2C89: 20 63 30
; L_BRS_($2C8C)_($2C76) OK
Chg_Obj_To_Explosion_Then_Next
jsr Change_Object_To_Explosion ; 2C8C: 20 F9 28
jmp GOTO_NEXT_OBJECT ; 2C8F: 4C 1A 26

'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.

; ############################################################################################
; Yb dP db Yb dP 888888 .d
; Yb db dP dPYb Yb dP 88__ .d88
; YbdPYbdP dP__Yb YbdP 88"" 88
; YP YP dP""""Yb YP 888888 88
; dP""b8 88""Yb dP"Yb .dP"Y8 .dP"Y8
; dP `" 88__dP dP Yb `Ybo." `Ybo."
; Yb 88"Yb Yb dP o.`Y8b o.`Y8b
; YboodP 88 Yb YbodP 8bodP' 8bodP'
; ############################################################################################
WAVE_1_CROSS
jsr Test_Laser_Hits ; 2A5F: 20 5F 2C
bpl Wave_1_Cross_Direction_Update ; 2A62: 10 03
jmp Alien_Hit_Score_Update ; 2A64: 4C 78 2C
; ------------------------------------------
; Alien not hit by laser, check to see if hit platform, aliens bounce off after first wave, so
; update direction
; L_BRS_($2A67)_($2A62) OK
Wave_1_Cross_Direction_Update
jsr Test_Platform_Collision_Bounce ; 2A67: 20 16 2B
jmp Direction_X_Y_Update ; 2A6A: 4C 83 2A

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.

; ############################################################################################
; .dP"Y8 88 88 88 88""Yb db .dP"Y8 dP""b8 888888 88b 88 8888b.
; `Ybo." 88 88 88 88__dP dPYb `Ybo." dP `" 88__ 88Yb88 8I Yb
; o.`Y8b 888888 88 88""" dP__Yb o.`Y8b Yb 88"" 88 Y88 8I dY
; 8bodP' 88 88 88 88 dP""""Yb 8bodP' YboodP 888888 88 Y8 8888Y"
; ############################################################################################
; Aliens keep moving also
SHIP_ASCEND
jsr Load_Object_Type_X_Y ; 2E17: 20 D7 35
; Test if Ship hit top of screen yet, if yes branch
ldy #SHIP_BASE_POSITION_Y_PARAM ; 2E1A: A0 03
lda (ZP_Obj_List_Ptr_Lo),y ; 2E1C: B1 00
cmp #SHIP_BASE_AT_SCREEN_TOP ; 2E1E: C9 30
bcc Ship_At_Top ; 2E20: 90 12
; Calculate and store new height i.e one pixel up screen
sec ; 2E22: 38
sbc #$01 ; 2E23: E9 01
sta (ZP_Obj_List_Ptr_Lo),y ; 2E25: 91 00
; Trigger ship flight sound
lda #$04 ; 2E27: A9 04
sta Obj_Sound_Noise ; 2E29: 8D AB 03
lda #$02 ; 2E2C: A9 02
sta Obj_Sound_Noise_Timer ; 2E2E: 8D AC 03
jmp Display_Ship ; 2E31: 4C AC 2E
; ------------------------------------------
; L_BRS_($2E34)_($2E20) OK
Ship_At_Top
inc ZP_Wave_Active_Player ; 2E34: E6 59
jsr Reset_Objects_And_Aliens ; 2E36: 20 C9 28
; Transition ship object type from ascend to descend
ldy #OBJECT_TYPE_PARAM ; 2E39: A0 00
lda #OBJECT_SHIP_DESCEND ; 2E3B: A9 0B
sta (ZP_Obj_List_Ptr_Lo),y ; 2E3D: 91 00
; Zero the ship's fuel level
tya ; 2E3F: 98
ldy #OBJECT_SHIP_FUEL_LEVEL ; 2E40: A0 02
sta (ZP_Obj_List_Ptr_Lo),y ; 2E42: 91 00
; Turn off the flame animation
jsr Reset_Level_Add_Extra_Life ; 2E44: 20 BA 22
lda #$00 ; 2E47: A9 00
sta ZP_Ship_Flame_State ; 2E49: 85 26
jmp GOTO_NEXT_OBJECT ; 2E4B: 4C 1A 26
; ------------------------------------------
; ############################################################################################
; .dP"Y8 88 88 88 88""Yb 8888b. 888888 .dP"Y8 dP""b8 888888 88b 88 8888b.
; `Ybo." 88 88 88 88__dP 8I Yb 88__ `Ybo." dP `" 88__ 88Yb88 8I Yb
; o.`Y8b 888888 88 88""" 8I dY 88"" o.`Y8b Yb 88"" 88 Y88 8I dY
; 8bodP' 88 88 88 88 8888Y" 888888 8bodP' YboodP 888888 88 Y8 8888Y"
; ############################################################################################
SHIP_DESCEND
jsr Load_Object_Type_X_Y ; 2E4E: 20 D7 35
; Trigger ship flight sound
lda #$04 ; 2E51: A9 04
sta Obj_Sound_Noise ; 2E53: 8D AB 03
lda #$02 ; 2E56: A9 02
sta Obj_Sound_Noise_Timer ; 2E58: 8D AC 03
; Move object Y position down 1 pixel, stopping at bottom i.e. $AF
ldy #SHIP_BASE_POSITION_Y_PARAM ; 2E5B: A0 03
lda (ZP_Obj_List_Ptr_Lo),y ; 2E5D: B1 00
clc ; 2E5F: 18
adc #$01 ; 2E60: 69 01
sta (ZP_Obj_List_Ptr_Lo),y ; 2E62: 91 00
; Test if ship has landed
cmp #SHIP_BASE_POSITION_Y ; 2E64: C9 AF
bcc Display_Ship ; 2E66: 90 44
; Set current object type to regular ship base part
ldy #OBJECT_TYPE_PARAM ; 2E68: A0 00
lda #OBJECT_SHIP_LANDED ; 2E6A: A9 09
sta (ZP_Obj_List_Ptr_Lo),y ; 2E6C: 91 00
jsr Init_Next_Jetman_Lives_Scores ; 2E6E: 20 5B 23
jmp Display_Ship ; 2E71: 4C AC 2E

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.

; ############################################################################################
; .dP"Y8 dP"Yb 88 88 88b 88 8888b. 88 88 88""Yb 8888b. db 888888 888888
; `Ybo." dP Yb 88 88 88Yb88 8I Yb 88 88 88__dP 8I Yb dPYb 88 88__
; o.`Y8b Yb dP Y8 8P 88 Y88 8I dY Y8 8P 88""" 8I dY dP__Yb 88 88""
; 8bodP' YbodP `YbodP' 88 Y8 8888Y" `YbodP' 88 8888Y" dP""""Yb 88 888888
; ############################################################################################
; Init sound object parameters (i.e. 8 bytes) from static data table
; L_JMP_($26AE)_($22CE) OK
Initialize_Sound_Object
ldx #$07 ; 26AE: A2 07
; L_BRS_($26B0)_($26B7) OK
Loop_Initialize_Sound_Object
lda Init_Sound_Object_Table,x ; 26B0: BD C5 26
sta Obj_Sound,x ; 26B3: 9D A8 03
dex ; 26B6: CA
bpl Loop_Initialize_Sound_Object ; 26B7: 10 F7
; Initialize sound channels
lda #$00 ; 26B9: A9 00
sta VIC_RA_Frq_Osc1 ; 26BB: 8D 0A 90
sta VIC_RB_Frq_Osc2 ; 26BE: 8D 0B 90
sta VIC_RC_Frq_Osc3 ; 26C1: 8D 0C 90
rts ; 26C4: 60
; ==========================================
Init_Sound_Object_Table
fcb $0C,$00,$00,$00 ; 26C5: 0C 00 00 00
fcb $00,$00,$00,$00 ; 26C9: 00 00 00 00
; ==========================================
; Read and play sound channel data from the Sound object
SOUND_UPDATE
lda #$01 ; 26CD: A9 01
sta ZP_Subroutine_Addr_Hi ; 26CF: 85 60
; Sound object parameter 01/03/05 indicates which sound to play, 02/04/06 is the period
; L_BRS_($26D1)_($26ED) OK
Loop_Sound_Update
ldy ZP_Subroutine_Addr_Hi ; 26D1: A4 60
lda (ZP_Obj_List_Ptr_Lo),y ; 26D3: B1 00
asl a ; 26D5: 0A
tax ; 26D6: AA
; Jump to Sound player using Register X as jump table index
lda Sound_Object_Jump_Table,x ; 26D7: BD 43 27
sta ZP08_Subroutine_Addr_Lo ; 26DA: 85 08
lda Sound_Object_Jump_Table+1,x ; 26DC: BD 44 27
sta ZP09_Subroutine_Addr_Hi ; 26DF: 85 09
jmp (ZP08_Subroutine_Addr_Lo) ; 26E1: 6C 08 00
; ------------------------------------------
; Update index to next 2-byte oscillator/timer pair, stopping when
; all 3 processed i.e. at 7th parameter. Sound 0 is valid
; but 'empty' sound call, so just moves to next 2-byte oscillator/timer pair.
; L_JMP_($26E4)_($2719) OK
Goto_Next_Sound
lda ZP_Subroutine_Addr_Hi ; 26E4: A5 60
clc ; 26E6: 18
adc #WORD_SIZE ; 26E7: 69 02
sta ZP_Subroutine_Addr_Hi ; 26E9: 85 60
cmp #$07 ; 26EB: C9 07
bcc Loop_Sound_Update ; 26ED: 90 E2
jmp GOTO_NEXT_OBJECT ; 26EF: 4C 1A 26

VALUABLES objects have movement, can be picked up by Jetman and can change colour, depending on which valuable they are.

Like for the SOUND UPDATE object, the handler uses a valuable type jump table to jump to the appropriate handler.

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.

JETPAC Object Handler DISPLAY LASERS

A summary of the flowcharted routine:

  1. 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.

  2. 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).

  3. 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.

  4. 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.

; 88 dP"Yb db 8888b. 8888P 88""Yb
; 88 dP Yb dPYb 8I Yb dP 88__dP
; 88 .o Yb dP dP__Yb 8I dY dP 88"""
; 88ood8 YbodP dP""""Yb 8888Y" d8888 88
; 88""Yb db 88""Yb db 8b d8 888888 888888 888888 88""Yb .dP"Y8
; 88__dP dPYb 88__dP dPYb 88b d88 88__ 88 88__ 88__dP `Ybo."
; 88""" dP__Yb 88"Yb dP__Yb 88YbdP88 88"" 88 88"" 88"Yb o.`Y8b
; 88 dP""""Yb 88 Yb dP""""Yb 88 YY 88 888888 88 888888 88 Yb 8bodP'
; Parameters for this routine are assembled into memory directly after the
; call to this routine. When the routine is called using JSR, the return address
; will be the address of the first parameter, so they can be pulled off using PLA.
; First two stack values are the 16-bit address where next two stack values will be stored.
; L_JSR_($259D)_($20F0) OK
; L_JSR_($259D)_($2202) OK
; L_JSR_($259D)_($22E6) OK
; L_JSR_($259D)_($22F5) OK
; L_JSR_($259D)_($2311) OK
; L_JSR_($259D)_($2373) OK
; L_JSR_($259D)_($2382) OK
; L_JSR_($259D)_($2396) OK
; L_JSR_($259D)_($2471) OK
; L_JSR_($259D)_($2481) OK
; L_JSR_($259D)_($2491) OK
; L_JSR_($259D)_($24CA) OK
; L_JSR_($259D)_($24D7) OK
; L_JSR_($259D)_($24E4) OK
; L_JSR_($259D)_($2676) OK
; L_JSR_($259D)_($277A) OK
; L_JSR_($259D)_($2787) OK
; L_JSR_($259D)_($2867) OK
; L_JSR_($259D)_($29EE) OK
; L_JSR_($259D)_($29FB) OK
; L_JSR_($259D)_($2A12) OK
; L_JSR_($259D)_($2A48) OK
; L_JSR_($259D)_($2BCA) OK
; L_JSR_($259D)_($2DB8) OK
; L_JSR_($259D)_($2E8F) OK
; L_JSR_($259D)_($2FF9) OK
; L_JSR_($259D)_($3067) OK
; L_JSR_($259D)_($3091) OK
; L_JSR_($259D)_($3469) OK
; L_JSR_($259D)_($349A) OK
; L_JSR_($259D)_($355C) OK
; L_JSR_($259D)_($356C) OK
Load_ZP_Parameters
pla ; 259D: 68
sta ZP_Param_Addr_Lo ; 259E: 85 40
pla ; 25A0: 68
sta ZP_Param_Addr_Hi ; 25A1: 85 41
ldy #$01 ; 25A3: A0 01
; $FF signifies end of address/parameter data to load
; L_JMP_($25A5)_($25C7) OK
Loop_Test_Param_List_End
lda (ZP_Param_Addr_Lo),y ; 25A5: B1 40
cmp #$FF ; 25A7: C9 FF
bne Load_ZP_Parameter ; 25A9: D0 0A
; All parameters now loaded, push a new return address back onto the stack and RTS
jsr Inc_Param_Addr ; 25AB: 20 CA 25
lda ZP_Param_Addr_Hi ; 25AE: A5 41
pha ; 25B0: 48
lda ZP_Param_Addr_Lo ; 25B1: A5 40
pha ; 25B3: 48
rts ; 25B4: 60
; ==========================================
; Register A contain 8-bit ZP address to store 16-bit value to, Register Y is 1
; L_BRS_($25B5)_($25A9) OK
Load_ZP_Parameter
tax ; 25B5: AA
; Increment the param address, load the data stored there and store it to a ZP location
jsr Inc_Param_Addr ; 25B6: 20 CA 25
lda (ZP_Param_Addr_Lo),y ; 25B9: B1 40
sta ZP_Obj_List_Ptr_Lo,x ; 25BB: 95 00
; Inc the param address again, load the data stored there and store it to a ZP location
jsr Inc_Param_Addr ; 25BD: 20 CA 25
lda (ZP_Param_Addr_Lo),y ; 25C0: B1 40
sta ZP_Obj_List_Ptr_Hi,x ; 25C2: 95 01
; Inc the param address again and loop back to process next data value
jsr Inc_Param_Addr ; 25C4: 20 CA 25
jmp Loop_Test_Param_List_End ; 25C7: 4C A5 25
; ------------------------------------------
; Increment the 16-bit parameter address, lo-byte then hi-byte if necessary
; L_JSR_($25CA)_($25AB) OK
; L_JSR_($25CA)_($25B6) OK
; L_JSR_($25CA)_($25BD) OK
; L_JSR_($25CA)_($25C4) OK
Inc_Param_Addr
inc ZP_Param_Addr_Lo ; 25CA: E6 40
bne Inc_Param_Addr_RTS ; 25CC: D0 02
inc ZP_Param_Addr_Hi ; 25CE: E6 41
; L_BRS_($25D0)_($25CC) OK
Inc_Param_Addr_RTS
rts ; 25D0: 60

The game utilizes a VIA timer to:

  • Prioritize object list updates to the Jetman object (so he's always responsive)

  • Create a pseudo-random number generator, in conjunction with the TV Raster

  • Increments 16-bit counter used in various places in the code.

; +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
IRQ_Interrupt_Handler
lda #$01 ; 25E0: A9 01
; Set IRQ Occured flag, used to prioritise Jetman object handling in main loop
sta ZP_IRQ_Occurred ; 25E2: 85 20
; Reset VIA2 timer, which directly regulates Jetman's update speed
jsr Reset_VIA2_Timer ; 25E4: 20 5E 26
; Use TV Raster value as a pseudo-random number generator
lda VIC_R4_TV_Raster ; 25E7: AD 04 90
adc ZP_IRQ_Random ; 25EA: 65 4C
sta ZP_IRQ_Random ; 25EC: 85 4C
; Increment 16-bit counter
inc ZP_IRQ_Counter_Lo ; 25EE: E6 4A
bne Restore_Registers_And_RTI ; 25F0: D0 02
inc ZP_IRQ_Counter_Hi ; 25F2: E6 4B
; L_BRS_($25F4)_($25F0) OK
Restore_Registers_And_RTI
pla ; 25F4: 68
tay ; 25F5: A8
pla ; 25F6: 68
tax ; 25F7: AA
pla ; 25F8: 68
rti ; 25F9: 40
; +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-

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.

; 88 88b 88 888888 888888 88""Yb 88""Yb 88 88 88""Yb 888888
; 88 88Yb88 88 88__ 88__dP 88__dP 88 88 88__dP 88
; 88 88 Y88 88 88"" 88"Yb 88"Yb Y8 8P 88""" 88
; 88 88 Y8 88 888888 88 Yb 88 Yb `YbodP' 88 88
; 88 88 db 88b 88 8888b. 88 888888 88""Yb
; 88 88 dPYb 88Yb88 8I Yb 88 88__ 88__dP
; 888888 dP__Yb 88 Y88 8I dY 88 .o 88"" 88"Yb
; 88 88 dP""""Yb 88 Y8 8888Y" 88ood8 888888 88 Yb
; ----------------------------------------------------
; VIA2 timer interrupts used to update Jetman movement
; $4FD7 / 1.108Mhz = 54.2Hz interrupt. Screen is 184 pixels
; wide. At one pixel walking movement per interrupt, Jetman
; should get across the screen in about 3.5 seconds.
; ----------------------------------------------------
; L_JSR_($265E)_($2041) OK
; L_JSR_($265E)_($25E4) OK
Reset_VIA2_Timer
lda #$D7 ; 265E: A9 D7
sta VIA2_T1_Cnt_Lo ; 2660: 8D 24 91
lda #$4F ; 2663: A9 4F
sta VIA2_T1_Cnt_Hi ; 2665: 8D 25 91
rts ; 2668: 60
; ==========================================
; Disable Interrupts (Set Interupt Disable)
; Save current object address pointer and set Jetman to be the next object
; L_BRS_($2669)_($2639) OK
SEI_Obj_Ptr_To_Jetman
sei ; 2669: 78
lda #$00 ; 266A: A9 00
sta ZP_IRQ_Occurred ; 266C: 85 20
; Save the current object list ptr and load up the Jetman object parameters
lda ZP_Obj_List_Ptr_Lo ; 266E: A5 00
sta ZP_Obj_List_Ptr_Save_IRQ_Lo ; 2670: 85 22
lda ZP_Obj_List_Ptr_Hi ; 2672: A5 01
sta ZP_Obj_List_Ptr_Save_IRQ_Hi ; 2674: 85 23
; Load Jetman object parameters
jsr Load_ZP_Parameters ; 2676: 20 9D 25
fcb $00 ; 2679: 00 ZP00/01
fdb Obj_Jetman_State ; 267A: 80 03
fcb $FF ; 267C: FF
jmp Main_Loop ; 267D: 4C 07 26

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 collision test can result in a Fuel Cell or Ship Module object being picked up and following Jetman around.

Note the picked-up object isn't updated on-screen as often as Jetman so they move along in jumps after Jetman.

In my mind, this characteristic is one of the memorable aspects of JETPAC, giving the game an extra liveliness.

; 88888 888888 888888 8b d8 db 88b 88
; 88 88__ 88 88b d88 dPYb 88Yb88
; o. 88 88"" 88 88YbdP88 dP__Yb 88 Y88
; "bodP' 888888 88 88 YY 88 dP""""Yb 88 Y8
; dP""b8 dP"Yb 88 88 88 8888b. 888888
; dP `" dP Yb 88 88 88 8I Yb 88__
; Yb Yb dP 88 .o 88 .o 88 8I dY 88""
; YboodP YbodP 88ood8 88ood8 88 8888Y" 888888
; .dP"Y8 88 88 88 88""Yb 888888 88 88 888888 88
; `Ybo." 88 88 88 88__dP 88__ 88 88 88__ 88
; o.`Y8b 888888 88 88""" 88"" Y8 8P 88"" 88 .o
; 8bodP' 88 88 88 88 88 `YbodP' 888888 88ood8
; At point when Jetman picks up Ship or Fuel module
; L_BRS_($2FEE)_($2FA9) OK
Jetman_Collide_Ship_Fuel
jsr Load_Object_Type_X_Y ; 2FEE: 20 D7 35
ldy #OBJECT_STATUS_PARAM ; 2FF1: A0 04
lda (ZP_Obj_List_Ptr_Lo),y ; 2FF3: B1 00
ora #OBJECT_STATUS_PICKED_UP ; 2FF5: 09 02
sta (ZP_Obj_List_Ptr_Lo),y ; 2FF7: 91 00
; Jetman pickup Ship Module / Fuel Cell = 100 points
jsr Load_ZP_Parameters ; 2FF9: 20 9D 25
fcb $02 ; 2FFC: 02 ZP02/03
fdb $0100 ; 2FFD: 00 01 100 Points
fcb $FF ; 2FFF: FF
jsr Update_Score ; 3000: 20 63 30
; Trigger collision pickup sound
lda #$01 ; 3003: A9 01
sta Obj_Sound_Collision ; 3005: 8D A9 03
lda #$10 ; 3008: A9 10
sta Obj_Sound_Collision_Timer ; 300A: 8D AA 03
; Store current Jetman Position X into current picked-up object
ldy #JETMAN_POSITION_X_PARAM ; 300D: A0 01
lda Obj_Jetman_Position_X ; 300F: AD 81 03
sta (ZP_Obj_List_Ptr_Lo),y ; 3012: 91 00
; Store current Jetman Position Y into current picked-up object
ldy #JETMAN_POSITION_Y_PARAM ; 3014: A0 03
lda Obj_Jetman_Position_Y ; 3016: AD 83 03
sta (ZP_Obj_List_Ptr_Lo),y ; 3019: 91 00
jmp Setup_Object_Addr_Ship_Fuel ; 301B: 4C BD 2F

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.

JETPAC Object Handler Test Platform Collision

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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

JETPAC Keyboard Controls

; 88 dP 888888 Yb dP 88""Yb dP"Yb db 88""Yb 8888b.
; 88odP 88__ YbdP 88__dP dP Yb dPYb 88__dP 8I Yb
; 88"Yb 88"" 8P 88""Yb Yb dP dP__Yb 88"Yb 8I dY
; 88 Yb 888888 dP 88oodP YbodP dP""""Yb 88 Yb 8888Y"
; 88888 dP"Yb Yb dP .dP"Y8 888888 88 dP""b8 88 dP
; 88 dP Yb YbdP `Ybo." 88 88 dP `" 88odP
; o. 88 Yb dP 8P o.`Y8b 88 88 Yb 88"Yb
; "bodP' YbodP dP 8bodP' 88 88 YboodP 88 Yb
; VIA Port A Bit 2=Up, 3=Down, 4=Left, 5=Fire
; Port A read twice, 1st for Up/Down/Left, 2nd for Fire
; Port A reads $7C=%0111_1100 when nothing pressed i.e. joystick inputs are active low
; VIA Port B Bit 7=Left
; Port B read once for Right
; Port B reads $FF=%1111_1111 when nothing pressed i.e. joystick inputs are active low
; L_BRS_($33D4)_($33FD) OK
Read_Joystick
lda VIA1_Port_A ; 33D4: AD 11 91 %xxxL_DUxx
rol a ; 33D7: 2A
rol a ; 33D8: 2A
ora #%10001111 ; 33D9: 09 8F %1LDU_1111
sta ZP_Jetman_Action ; 33DB: 85 1C $FF when nothing pressed
; Read Fire button
lda VIA1_Port_A ; 33DD: AD 11 91 %xxFx_xxxx
ror a ; 33E0: 6A
ror a ; 33E1: 6A
ora #%11110111 ; 33E2: 09 F7 %1111_F111
and ZP_Jetman_Action ; 33E4: 25 1C %1LDU_F111
sta ZP_Jetman_Action ; 33E6: 85 1C $FF when nothing pressed
; Read direction Right
lda #$00 ; 33E8: A9 00
sta VIA2_DDR_B ; 33EA: 8D 22 91 Set Port B direction all lines read
lda VIA2_Port_B ; 33ED: AD 20 91 %Rxxx_xxxx
ora #%01111111 ; 33F0: 09 7F %R111_1111
and ZP_Jetman_Action ; 33F2: 25 1C %RLDU_F111
sta ZP_Jetman_Action ; 33F4: 85 1C $FF when nothing pressed
rts ; 33F6: 60
; ==========================================
; If joystick selected, branch and return without reading keyboard
; L_JSR_($33F7)_($3276) OK
; L_JSR_($33F7)_($336D) OK
Read_Keyboard_Joystick
lda Game_Settings ; 33F7: AD D3 1F
bit MASK_00000010 ; 33FA: 2C 15 20
bne Read_Joystick ; 33FD: D0 D5
; Read Keyboard, only executed when joystick not selected
; Setup outputs to enable reading of Keyboard matrix
lda #%11111111 ; 33FF: A9 FF Set Port A to all output
sta VIA2_DDR_A ; 3401: 8D 23 91
lda #%10000001 ; 3404: A9 81 Set Port A outputs
sta VIA2_Port_A ; 3406: 8D 21 91
lda #%00000000 ; 3409: A9 00 Set Port B to all inputs
sta VIA2_DDR_B ; 340B: 8D 22 91
; Read keyboard matrix, multiple keys can be pressed for each direction and fire, see
; hardware documentation for more detail
; Read right & left input keys at the same time (L: Z C B M . RSHFT) (R: LSHFT X V N , /)
lda VIA2_Port_B ; 340E: AD 20 91 %xxxR_Lxxx
rol a ; 3411: 2A
rol a ; 3412: 2A
rol a ; 3413: 2A
ora #%00111111 ; 3414: 09 3F %RL11_1111
sta ZP_Jetman_Action ; 3416: 85 1C %RL11_1111
; Read Fire input keys, set 1 (S F H K : =)
lda VIA2_Port_B ; 3418: AD 20 91 %xxFx_xxxx
tay ; 341B: A8
ror a ; 341C: 6A
ror a ; 341D: 6A
ora #%11110111 ; 341E: 09 F7 %1111_F111
and ZP_Jetman_Action ; 3420: 25 1C
sta ZP_Jetman_Action ; 3422: 85 1C %RL11_F111
; Read Fire input keys, set 2 (A D G J L ;)
tya ; 3424: 98 %xxxx_xFxx
rol a ; 3425: 2A
ora #%11110111 ; 3426: 09 F7 %1111_F111
and ZP_Jetman_Action ; 3428: 25 1C
sta ZP_Jetman_Action ; 342A: 85 1C %RL11_F111
; Read Up input keys, set 1 (Q E T U O @ UA)
lda #%10000000 ; 342C: A9 80
sta VIA2_Port_A ; 342E: 8D 21 91
lda VIA2_Port_B ; 3431: AD 20 91 %xUxx_xxxx
ror a ; 3434: 6A
ror a ; 3435: 6A
ora #%11101111 ; 3436: 09 EF %111U_1111
and ZP_Jetman_Action ; 3438: 25 1C
sta ZP_Jetman_Action ; 343A: 85 1C %RL1U_F111
; Read Up input keys, set 2 (W R Y I P *)
lda #%10000001 ; 343C: A9 81
sta VIA2_Port_A ; 343E: 8D 21 91
lda VIA2_Port_B ; 3441: AD 20 91 %xxxx_xxUx
rol a ; 3444: 2A
rol a ; 3445: 2A
rol a ; 3446: 2A
ora #%11101111 ; 3447: 09 EF %111U_1111
and ZP_Jetman_Action ; 3449: 25 1C
sta ZP_Jetman_Action ; 344B: 85 1C %RL1U_F111
; Read Down input keys, set 1 and 2 (2 4 6 8 0 - HOM) & (1 3 5 7 9 + BP)
lda #%10000000 ; 344D: A9 80
sta VIA2_Port_A ; 344F: 8D 21 91
lda VIA2_Port_B ; 3452: AD 20 91 %Dxxx_xxxD
tay ; 3455: A8
jsr Read_Keyboard_Down ; 3456: 20 5C 34
tya ; 3459: 98
ror a ; 345A: 6A
ror a ; 345B: 6A
; L_JSR_($345C)_($3456) OK
Read_Keyboard_Down
ror a ; 345C: 6A
ror a ; 345D: 6A
ora #%11011111 ; 345E: 09 DF %11D1_1111
and ZP_Jetman_Action ; 3460: 25 1C
sta ZP_Jetman_Action ; 3462: 85 1C %RLDU_F111
rts ; 3464: 60

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.

; dP""b8 dP"Yb 88b 88 Yb dP 888888 88""Yb 888888 Yb dP Yb dP
; dP `" dP Yb 88Yb88 Yb dP 88__ 88__dP 88 YbdP YbdP
; Yb Yb dP 88 Y88 YbdP 88"" 88"Yb 88 dPYb 8P
; YboodP YbodP 88 Y8 YP 888888 88 Yb 88 dP Yb dP
; 888888 dP"Yb 88 88 8888b. dP""b8 88""Yb db 8b d8
; 88 dP Yb 88 88 8I Yb dP `" 88__dP dPYb 88b d88
; 88 Yb dP Y8 8P 8I dY Yb "88 88"Yb dP__Yb 88YbdP88
; 88 YbodP `YbodP' 8888Y" YboodP 88 Yb dP""""Yb 88 YY 88
; db 8888b. 8888b. 88""Yb 888888 .dP"Y8 .dP"Y8
; dPYb 8I Yb 8I Yb 88__dP 88__ `Ybo." `Ybo."
; dP__Yb 8I dY 8I dY 88"Yb 88"" o.`Y8b o.`Y8b
; dP""""Yb 8888Y" 8888Y" 88 Yb 888888 8bodP' 8bodP'
; Example for "F7 JOYSTICK" start position
; Convert X=$18 pixels from left, Y=$68 pixels from top, to UDG RAM Address $1278
; L_JSR_($351D)_($21BE) OK
; L_JSR_($351D)_($226E) OK
; L_JSR_($351D)_($24FD) OK
; L_JSR_($351D)_($3168) OK
; L_JSR_($351D)_($3470) OK
; L_JSR_($351D)_($34CF) OK
; L_JSR_($351D)_($375D) OK
; L_JSR_($351D)_($37AB) OK
Convert_XY_to_UDG_RAM_Address
lda ZP05_Position_Y ; 351D: A5 05 ZP05 = %0110_1000 = $68
tay ; 351F: A8
; Divide X by 8 to get address Lo-byte
lda ZP04_Position_X ; 3520: A5 04 ZP04 = %0001_1000 = $18
lsr a ; 3522: 4A
lsr a ; 3523: 4A
lsr a ; 3524: 4A ZP04 = %0000_0011 = $03
tax ; 3525: AA
; Load from column offset table
lda UDG_RAM_Column_Offset_Table,x ; 3526: BD 45 35 A = $21
sta ZP04_Position_X ; 3529: 85 04
lda #$00 ; 352B: A9 00
sta ZP05_Position_Y ; 352D: 85 05 ZP05/04 = $0021
; Rotate 16-bit address left by 4 bits
ldx #$04 ; 352F: A2 04
; L_BRS_($3531)_($3536) OK
Loop_Rotate
asl ZP04_UDG_RAM_Addr_Lo ; 3531: 06 04
rol ZP05_UDG_RAM_Addr_Hi ; 3533: 26 05
dex ; 3535: CA
bne Loop_Rotate ; 3536: D0 F9 ZP05/04 $0021 > $0210
; Add Register Y saved earlier to address in ZP05/04
tya ; 3538: 98 Register A becomes $68
clc ; 3539: 18
adc ZP04_UDG_RAM_Addr_Lo ; 353A: 65 04
sta ZP04_UDG_RAM_Addr_Lo ; 353C: 85 04 ZP05/04 = $0278
; Add $1000 to address in ZP05/04 to become $1278 i.e. "F7 JOYSTICK" start position
lda #$10 ; 353E: A9 10
adc ZP05_UDG_RAM_Addr_Hi ; 3540: 65 05
sta ZP05_UDG_RAM_Addr_Hi ; 3542: 85 05 ZP05/04 = $1278
rts ; 3544: 60
; ==========================================
; Bytes are middle two numbers in UDG RAM addr starting at $1000 e.g. $10B0, $1160 etc.
UDG_RAM_Column_Offset_Table
fcb $00,$0B,$16,$21,$2C,$37,$42,$4D ; 3545: 00 0B 16 21 2C 37 42 4D
fcb $58,$63,$6E,$79,$84,$8F,$9A,$A5 ; 354D: 58 63 6E 79 84 8F 9A A5
fcb $B0,$BB,$C6,$D1,$DC,$E7,$F2 ; 3555: B0 BB C6 D1 DC E7 F2

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.

JETPAC Object Handler Display Object

A summary of the flowcharted routine:

  1. 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.

  2. 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.

  3. 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.

; dP""b8 dP"Yb 88 dP"Yb 88""Yb 88 8888P 888888
; dP `" dP Yb 88 dP Yb 88__dP 88 dP 88__
; Yb Yb dP 88 .o Yb dP 88"Yb 88 dP 88""
; YboodP YbodP 88ood8 YbodP 88 Yb 88 d8888 888888
; dP"Yb 88""Yb 88888 888888 dP""b8 888888
; dP Yb 88__dP 88 88__ dP `" 88
; Yb dP 88""Yb o. 88 88"" Yb 88
; YbodP 88oodP "bodP' 888888 YboodP 88
; 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).

      00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16
--------------------------------------------------------------------------
9600  .. .. .. .. .. .. .. .. 07 07 07 07 07 07 .. .. .. .. .. .. .. .. ..    07=yellow
9617  .. .. .. .. .. .. .. .. .. .. .. .. 06 06 06 06 06 06 06 06 06 06 06    06=blue
962E  06 06 02 02 .. .. 02 02 02 02 02 .. 06 06 06 06 06 06 06 06 06 06 06    05=green
9645  06 06 02 02 .. .. 02 02 02 02 .. .. .. .. .. .. .. 05 05 05 05 05 06    04=purple
965C  06 03 05 05 05 05 05 .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..    03=cyan
9673  .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..    02=red
968A  02 02 02 02 .. .. .. .. .. .. 05 05 05 .. .. .. .. .. .. .. .. .. ..    01=white
96A1  02 02 02 02 02 02 02 02 .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..    00=black
96B8  .. .. 02 02 02 02 02 02 .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..
96CF  .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..
96E6  .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. ..

User-Defined Graphics Data Lookup Tables

The graphics data is mostly referenced by two address lookup tables.

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.

Back to top

About

A reverse-engineering of the classic VIC-20 game, JETPAC, by ULTIMATE PLAY THE GAME.

Resources

Stars

Watchers

Forks

Languages