Lunar Lander (1979) disassembly

This is an intro the the disassembly of Atari's 1979 Lunar Lander vector arcade game. There is also the full listing, llander.asm.

Intro to assembly

It's helpful to have the instruction set reference open in another tab to help answer any questions about the semantics of each instruction and the addressing modes.

Some common idioms in the code:

  • Arithmetic comparisons and branches (note that the C flag seems backwards since it is set if A is greater or equal to the comparison value, not the other way around)
    	LDA variable	; Read the global variable into the A register
    	BMI if_neg	; If A < 0, take this branch
    	BPL if_pos	; If A >= 0, take this branch
    	BEQ if_zero	; If A == 0, take this branch
    	CMP #$25	; Compute A - 0x25, but do not store the result.  Set the flags N, Z, and C
    	BEQ if_eq	; If A == 0x25, the result was zero, so take this branch
    	BNE if_ne	; If A != 0x25, the result was non-zero, so take this one
    	BCS if_gt	; If A >= 0x25, take this branch (see note above)
    	BCC if_lt	; If A <  0x25, take this branch (see note above)
    

  • Bit tests of the two top bits
    	BIT variable	; Read the global variable, set the N and V status flags (also Z, but it's complicated)
    	BMI if_bit7	; If the 7th bit is set, take this branch
    	BPL if_not_bit7	; If the 7th bit is not set, take this branch
    	BVS if_bit6	; If the 6th bit is set, take this one
    	BVC if_not_bit6	; If the 6th bit is not set, take this one
    

  • Bit tests of all bits
    	LDA variable	; Read the global variable into the A register, set the N and Z flags
    	BNE if_any_bits	; If any bits are set, take this branch
    	BEQ if_no_bits	; If no bits are set, take this branch
    

  • 16-bit increment of a global variable
    	INC variable
    	BCC skip_inc
    	INC variable_high
    skip_in:
    	/* more stuff */
    

  • 16-bit addition of two global variables v1 and v2, writing into `v2
    	CLC		; start with the carry flag clear
    	LDA v1_low
    	ADC v2_low	; if the results overflows, the carry flag will be set
    	STA v2_low
    	LDA v1_high	; LDA does not change the carry flag
    	ADC v2_high	; carry flag added to the high bytes
    	STA v2_high
    

  • For loops indexing into an array (in this case computing the sum into A)
    	LDX #$03	; for x = 3, 2, 1, 0 == four passes through the loop
    loop:
    	CLC		; don't carry
    	ADC array,X	; A += array[X]
    	DEX		; x--
    	BPL loop	; if x >= 0 go again
    

    6502 Math

    The 6502 is an 8-bit CPU with 8-bit wide registers and an 8-bit wide data bus. There is no multiply instruction, so it is necessary to implement it in software. Some games, like BattleZone have a math coprocessor for doing 3D transforms, but Lunar Lander does it in software.

    Since the registers are 8-bits wide, passing a 16-bit value to a function requires two of them. Most of the time they are passed in A and X, but it is not consistent across all of the code in Lunar Lander. However a 16-by-16 multiply needs more registers, so some temporary zero page locations are used. The results are also left in zero page locations and can be used for chaning operations together.

    Minimum

    As a warmup, here's a function that returns the minimum of two 16-bit values that are stored in global variables on the zero page:

    min16:
    bcd_score_maybe+3fS711c+4fS711c+e7S711c+156
    72fe
    a0ff
    LDY #$ff
    ; Not sure what Y is used for in the return result
    7300
    a57e
    ; Assume A is higher, load high byte AH into A
    7302
    a67d
    ; and low byte AL into X
    7304
    c580
    ; Compare the high bytes of AH and BH
    7306
    9012
    ; AH is lower, return it
    7308
    d00c
    ; BH is lower, return it instead
    730a
    a47f
    ; Need to compare the low bytes, load BL into Y
    730c
    c47d
    ; Compare BL and AL
    730e
    b00a
    ; A is lower, return it
    7310
    a67f
    ; move BL into X
    7312
    a47d
    ; why is Y loaded here?
    7314
    9004
    ; jump to return B (note that AH=BH, so it does not need to be loaded)
    return_b:
    7308
    7316
    a580
    ; B is larger, move the BH into A
    7318
    a67f
    ; and BL into X
    return_ax:
    7306730e7314
    731a
    60
    RTS
    ; Return the 16-bit value in A:X

    To translate this into C with the same logic flow is not very idomatic, but hopefully makes it easier to see how it maps to the 8-bit math of the assembly:

    uint8_t a_high, a_low;
    uint8_t b_high, b_low;
    uint16_t min16(void)
    {
    	uint8_t a = a_high;
    	uint8_t x = a_low;
    

    if (a_high < b_high) goto ret;

    if (a_high > b_high) { a = b_high; x = b_low; goto ret; }

    if (a_low < b_low) x = b_low;

    ret: return a << 8 | x; }

    Multiply

    ; Multiply two 8-bit values in A and Y, returning a 16-bit value in A:X.
    ; Falls through to mult16_repeat
    mult16:
    debris_update+2bdebris_update+48debris_update+55ship_compute_accel_xy+2fship_compute_accel_xy+39fuel_drain_thrust+eFUN_706e+fexplode_something_lots_math+10
    70ef
    8543
    STA mult_a
    ; Store A into mult_a for repeated calls to mult16_repeat (which this now implicitly calls by falling through)
    ; Multiply the 8-bit values in Y with the previously used value in A
    mult16_repeat:
    FUN_706e+16explode_something_lots_math+17
    70f1
    8442
    STY mult_y
    ; Store Y into mult_y
    70f3
    a543
    LDA mult_a
    ; Load mult_a...
    70f5
    48
    PHA
    ; and store it on the stack
    70f6
    49ff
    EOR #$ff
    ; Invert the high byte (to make a test easier later)...
    70f8
    8543
    STA mult_a
    ; and store it back in mult_a
    70fa
    a900
    LDA #$00
    ; A = 0
    70fc
    8544
    ; Zero the multiply output low...
    70fe
    8545
    ; and high bytes
    7100
    a208
    LDX #$08
    ; Number of rounds (8 since this is an 8x8 to 16-bit multiply)
    mult_start_round:
    7119
    7102
    0643
    ASL mult_a
    ; Shift the inverted high byte to the left
    7104
    b006
    ; If the top bit was set, skip the increment
    7106
    6542
    ADC mult_y
    ; Add mult_y to A
    7108
    9002
    ; If this doesn't carry, skip updating the high byte
    710a
    e645
    ; The addition overflowed, increment the high byte
    mult_bit_not_set:
    71047108
    710c
    ca
    DEX
    ; Decrement our round counter
    710d
    d007
    ; If it is not zero, do another round
    710f
    aa
    TAX
    ; Move the multiply accumulator low-byte to X
    7110
    68
    PLA
    ; Restore the non-inverted mult_a from the stack,
    7111
    8543
    STA mult_a
    ; and store it back in the memory location
    7113
    a545
    ; Load the high byte of the multiply accumulator into A
    7115
    60
    RTS
    ; return the 16-bit value as the register pair X:A
    mult_continue_round:
    710d
    7116
    0a
    ASL
    ; Multiply the low-byte of the result by 2
    7117
    2645
    ; Multiply the high-byte by two, shifting in the carry
    7119
    90e7
    ; If this doesn't overflow, start another round

    I'm not certain about some of these operations; it seems that the mult_acc field is never used after being zeroed and I wonder if it is left over from a prior implementation. This code also causes a problem with the tracing disassembler since it appears that there is subroutine call to 0x711c, which is in the middle of an instruction. If the multiply does overflow, a BRK instruction is triggered that should halt the game.

    In any event, this algorithm could be translate roughly to the C code:

    uint16_t mult16(uint8_t a, uint8_t y) { uint8_t mult_acc = 0, mult_acc_high = 0; uint8_t inv_a = ~a; for(int8_t x = 8 ; x != 0 ; x--) { if (inv_a & 0x80) { mult_acc += y; if (mult_acc + y > 0xFF) mult_acc_high += 1; } mult_acc_high <<= 1; if (mult_acc & 0x80) mult_acc_high |= 1; mult_acc <<= 1; } return mult_acc_high << 8 | mult_acc; }

    16-bit signed magnitude math

    The core game update routine uses the 16-bit signed magnitude XY accelerations to compute the ship's velocity in the XY coordinate frame, which are then used to update the XY positions. Adding the values as part of the timestep update is implemented in this set of functions:

    ; Update delta1 with signed byte Y and 16-bit magnitude in A:X.
    ; Falls through...
    add16_signed_mag:
    ship_update+50S711c+a8S711c+194
    6cfe
    844d
    ;
    ; Update delta1 with 16-bit magnitude in A:X; does not modify the sign
    ; Falls through...
    add16_signed_mag_arg1:
    ship_update+2f
    6d00
    854b
    ;
    ; Update only the low-byte of of delta1 from X; does not modify the sign or high byte
    ; Falls through...
    add16_signed_mag_arg1_low:
    delta_something+22
    6d02
    864a
    STX delta1
    ;
    ; Compute the 16-bit signed addition of delta1 and delta2
    ; Returns the result in delta1 and delta1_sign, as well as X and A:Y
    ; Why is the return different from the calling convention?
    add16_signed_mag_core:
    ship_update+71S711c+cfS711c+1a7S711c+1c0S711c+1d2
    6d04
    a54c
    ; Compare the sign bits for delta1 and delta2
    6d06
    454d
    ; which are stored in the 7th bit of the byte
    6d08
    1034
    ; Same sign?
    6d0a
    38
    SEC
    ; Not the same sign, so this will be a delta1 - delta2
    6d0b
    a54a
    LDA delta1
    ; Subtract the low bytes of delta1
    6d0d
    e548
    SBC delta2
    ; and delta2.
    6d0f
    a8
    TAY
    ; Store this in Y
    6d10
    a54b
    ; Subtract the high bytes of delta1
    6d12
    e549
    ; and delta2, using the carry from 6d0d
    6d14
    9016
    ; If carry was cleared, an underflow occured
    6d16
    d00d
    ; Were some bits set in the high byte? Is so return them.
    6d18
    c000
    CPY #$00
    ; Were some bits set in the low byte in Y?
    6d1a
    d009
    ; If so, return them
    6d1c
    854a
    STA delta1
    ; Return result was exactly zero, so store zero in the low byte
    6d1e
    854b
    ; and zero the high byte
    6d20
    854d
    ; and reset the sign (no negative zero)
    6d22
    aa
    TAX
    ; Return low = 0
    6d23
    a8
    TAY
    ; Return high = 0
    6d24
    60
    RTS
    ; And return the triple
    return_delta1_sign:
    6d166d1a6d486d4d
    6d25
    a64d
    ; Return result has same sign as delta1
    return_delta1:
    6d3c
    6d27
    844a
    STY delta1
    ; Store low byte into Y
    6d29
    854b
    ; Store high byte into A
    6d2b
    60
    RTS
    ; And return the triple
    opposite_underflow:
    6d14
    6d2c
    49ff
    EOR #$ff
    ; The result of the subtraction underflowed, so invert the high byte in A
    6d2e
    aa
    TAX
    ; and move it to X
    6d2f
    98
    TYA
    ; Move the low byte of the subtraction from Y into A
    6d30
    49ff
    EOR #$ff
    ; Invert the low byte as well
    6d32
    a8
    TAY
    ; and move it back into Y
    6d33
    c8
    INY
    ; Increment Y, to undo the two's complement negative
    6d34
    d001
    ; If this also underflowed,
    6d36
    e8
    INX
    ; then also increment X to under the high-byte's two's complement negative
    opposite_2c_carry:
    6d34
    6d37
    8a
    TXA
    ; Move the high byte of the subtraction into A
    6d38
    a64c
    ; The underflowed result has the same sign as delta2 had
    6d3a
    864d
    ; Store the new sign byte into delta1 for the return
    6d3c
    90e9
    ; I think this is a branch always?
    same_sign:
    6d08
    6d3e
    18
    CLC
    ; Clear the carry
    6d3f
    a54a
    LDA delta1
    ; Add the low bytes of delta1
    6d41
    6548
    ADC delta2
    ; and delta2
    6d43
    a8
    TAY
    ; Store the result in Y
    6d44
    a54b
    ; Add the high bytes of delta1
    6d46
    6549
    ; and delta2, using the carry from the low-byte addition
    6d48
    90db
    ; If no overflow occured, return the result in delta1
    6d4a
    a9ff
    LDA #$ff
    ; Overflow, so clamp at 0xFFFF
    6d4c
    a8
    TAY
    ; in A:Y
    6d4d
    b0d6
    ; And return it with the same sign as delta1
    delta1_sign:
    ship_update+8add16_signed_mag_core+2add16_signed_mag_core+1cadd16_signed_mag+0delta_something+20add16_signed_mag_core+36add16_signed_mag_core+21
    004d
    .byte
    ; Sign byte for the 16-bit delta1
    delta1:
    S711c+acS711c+afadd16_signed_mag_arg1_low+0add16_signed_mag_core+7add16_signed_mag_core+18get_next_5bits+0get_next_5bits+2get_next_5bits+5get_next_5bits+9add16_signed_mag_core+3badd16_signed_mag_core+23S711c+197S711c+19a
    004a
    .word
    ; 16-bit magnitude for add16_signed_mag
    delta2_sign:
    ship_update+26ship_update+45ship_update+5dGameRunningLoop+13eGameRunningLoop+168S711c+9eS711c+cdadd16_signed_mag_core+0GameRunningLoop+260delta_something+1cadd16_signed_mag_core+34S711c+18cS711c+1b4
    004c
    .byte
    ; Sign byte for the 16-bit delta2
    delta2:
    ship_update+12ship_update+1bship_update+20ship_update+41ship_update+59ship_update+65ship_update+69ship_update+6bS711c+93S711c+b8add16_signed_mag_core+9FUN_706e+21delta_something+8delta_something+cdelta_something+14explode_something_lots_math+26explode_something_lots_math+36add16_signed_mag_core+3dS711c+183S711c+1a5S711c+1acS711c+1cb
    0048
    .word
    ; 16-bit magnitude for add16_signed_mag

    BCD

    The 6502 has a "Binary Coded Decimal" mode that only allows the values 0 - 9 for each four bits in a byte. This means that one byte can represent 00 to 99, and is frequently used by games to track scores or resources that are displayed to the player in base-10. On a modern system programmers would just use printf() or something to convert from binary to base-10, but that requires mutliply and divide operations that the 6502 did not have.

    Most of the fuel and score calculations in Lunar Lander are done in BCD, but there are other parts that are all done in binary, so occasionally it is necessary to convert between them. For these few times there is an interesting algorithm called Double Dabble that relatively efficiently produces a result with no multiplies or divides. If space is available, a lookup table is also an option.

    bcd_output[0]:
    bcd_score_maybe+23bcd_score_maybe+25score_add_landing_bonus+51score_add_landing_bonus+53score_add_landing_bonus+57score_add_landing_bonus+5bscore_add_landing_bonus+61dec_to_bcd+4dec_to_bcd+6dec_to_bcd+8dec_to_bcd+fdec_to_bcd+11dec_to_bcd+13dec_to_bcd+15dec_to_bcd+17dec_to_bcd+19dec_to_bcd+1bdec_to_bcd+1ddec_to_bcd+1fdec_to_bcd+24dec_to_bcd+26dec_to_bcd+28
    0092
    .byte[3]
    ; Working buffer for the dec_to_bcd function as well as its output

    ; Convert a 16-bit binary value into BCD, outputing to bcd_output or Y:X:A.
    ; Y:X input 16-bit value
    dec_to_bcd_16bit:
    DrawNumber_decimal_1+4bcd_score_maybe+51
    79c2
    8437
    ; Store MSB of argument in gb37
    79c4
    a00f
    LDY #$0f
    ; Pass 16 as the number of bits to convert
    ; Convert an arbitrary bit width binary value into BCD using the Double Dabble algorithm.
    ; Y: Number of bits to convert
    ; X: LSB of 16-bit input value
    ; GenByte_0038: MSB of the 16-bit input value
    ;
    ; Returns the data in bcd_output or in Y:X:A
    dec_to_bcd:
    fuel_drain_thrust+18bcd_score_maybe+11
    79c6
    8638
    ; Store LSB of argument in gb38
    79c8
    a900
    LDA #$00
    ; Zero the bcd output
    79ca
    8592
    ; lsb
    79cc
    8593
    ; mid
    79ce
    8594
    ; msb
    79d0
    f8
    SED
    ; turn on BCD mode
    dd_loop:
    79e8
    79d1
    0637
    ; D
    79d3
    2638
    ; -O
    79d5
    a592
    ; --U
    79d7
    6592
    ; ---B
    79d9
    8592
    ; ----L
    79db
    a593
    ; -----E
    79dd
    6593
    ; ------D
    79df
    8593
    ; -------A
    79e1
    a594
    ; --------B
    79e3
    6594
    ; ---------B
    79e5
    8594
    ; ----------L
    79e7
    88
    DEY
    ; -----------E
    79e8
    10e7
    ; if bits-- > 0 do it again
    79ea
    a494
    ; load the bcd output
    79ec
    a693
    ; into the registers
    79ee
    a592
    ; for some callers who want that
    79f0
    d8
    CLD
    ; back into binary mode
    79f1
    60
    RTS
    ; return to caller

    Cabinet buttons and lamps

    The Lakeside Arcade - Lunar Lander PCB Repair Logs has a helpful excerpt from the service manual showing the memory map for the arcade console:

    Memory map for the Lunar Lander 6502

    There are five buttons on the controls, Rotate Right, Rotate Left, Abort, Start and Select. These are all memory mapped into the 6502's address space and set bit 7 in the byte when they are pressed.

    IO_in1_start:
    RESET+1ecGameLoop+15e
    2400
    .byte
    ; Memory mapped active high button for start game
    IO_in1_select:
    mission_button_handler+0
    2404
    .byte
    ; Memory mapped active high button for select difficulty level
    IO_in1_abort:
    GameLoop+1bb
    2405
    .byte
    ; Memory mapped active high button for abort the mission
    IO_in1_yaw_right:
    IO_read_rotate_buttons+2
    2406
    .byte
    ; Memory mapped active high button for rotate to the right
    IO_in1_yaw_left:
    IO_read_rotate_buttons+8
    2407
    .byte
    ; Memory mapped active high button for rotate to the left

    There are also three coin slots mapped into the same memory region:

    IO_in1_coin[0]:
    CheckCoinsInserted+2
    2401
    .byte[3]
    ; Three memory mapped active high coin slots

    The lamps and sounds are also memory mapper peripherals. Eight output pins are mapped to the one address. The system caches the last value to avoid read-modify-write problems with this location.

    ; --------0 Attract lamp 0
    ; -------1- Attract lamp 1
    ; ------2-- Attract lamp 2
    ; -----3--- Attract lamp 3
    ; ----4---- Start/select LEDs
    ; ---5----- Coin counter enable
    IO_output_latch:
    RESET+127RESET+1dfRESET+265io_lamps_set+6
    3200
    .byte
    ; Lamps 0 - 3, Start, Select, Coin enable
    ; -----210 Thrust intensity
    ; ----3--- Thrust pitch
    ; --54--- Tone intensity
    IO_audio_latch:
    RESET+6RESET+7eRESET+99RESET+137RESET+13fRESET+27dio_audio_set+6
    3c00
    .byte
    ; Audio output control
    IO_audio_reset:
    ResetGameState+0
    3e00
    .byte
    ; Turn off the audio device

    To avoid read-modify-write cycles when updating the lamps or audio devices, the game keeps track of the last value written in a global variable and uses that as its cache. The functions take two parameters and act as a SET and RESET value.

    lamps_cache:
    io_lamps_set+0io_lamps_set+9
    0088
    .byte
    ; Cache of last value written to the lamp hardware
    audio_cache:
    RESET+1dbio_audio_set+0io_audio_set+9
    0089
    .byte
    ; Cache of last value written to the audio hardware
    io_genbyte:
    io_audio_set+2io_audio_set+4io_lamps_set+2io_lamps_set+4
    003c
    .byte
    ; Temp variable for the io functions

    ; Set the audio output hardware
    ;
    ; A = keep_bits
    ; X = set_bits
    ;
    ; last_set = set_bits
    ; cache = (keep & cache) | set_bits
    ; output = cache
    io_audio_set:
    RESET+20eRESET+26eGameLoop+feGameLoop+127GameLoop+130GameLoop+1f8debris_something+94draw_out_of_fuel_screen+35
    7953
    2589
    ; Mask the last written value to preserve the keep bits in A
    7955
    863c
    ; Store the set bits from X in the temp variable
    7957
    053c
    ; Set the set bits in A
    7959
    8d003c
    ; Write the new set bits and the kept bits out to the audio hardware
    795c
    8589
    ; Store this last written value in the cache
    795e
    60
    RTS
    ; And we're done!
    ; Set the lamps hardware
    ;
    ; A = keep_bits
    ; X = set_bits
    ;
    ; last_set = set_bits
    ; cache = (keep & cache) | set_bits
    ; output = cache
    io_lamps_set:
    NMI_handler+55NMI_handler+9bGameLoop+1eGameLoop+15bgame_state_reinit_maybe+49ship_reset+1c
    795f
    2588
    ; Mask the last written value to preserve the keep bits in A
    7961
    863c
    ; Store the set bits from X in the temp variable
    7963
    053c
    ; Set the set bits in A
    7965
    8d0032
    ; Write the new set bits and the kept bits out to the lamp hardware
    7968
    8588
    ; Store this last written value in the cache
    796a
    60
    RTS
    ; And we're done!

    Ship rotation

    Buttons

    The first function we're going to look at to keep things simple is the one that reads the player's rotation buttons.

    IO_read_rotate_buttons:
    ship_command_yaw+3dship_command_yaw_easy+4
    6404
    a200
    LDX #$00
    ; X = 0
    6406
    2c0624
    ; Read the right button from the IO port
    6409
    1001
    ; If bit 7 is not set, Button is not pressed, jump to 0x640c
    640b
    ca
    DEX
    ; X = X - 1
    not_right:
    6409
    640c
    2c0724
    ; Read the left button from the IO port
    640f
    1001
    ; Button is not pressed, jump to 0x6412
    6411
    e8
    INX
    ; X = X + 1
    not_left:
    640f
    6412
    8a
    TXA
    ; A = X
    6413
    60
    RTS
    ; Return 0 if neither or both buttons are pessed, -1 for right, +1 for left

    This could also be rewritten into something like C:

    volatile uint8_t * const IO_button_right = (void*) 0x2406;
    volatine uint8_t * const IO_button_left = (void*) 0x2407;
    int IO_read_rotate_buttons(void)
    {
      int yaw = 0;
      if (*IO_button_right & 0x80)
        yaw--;
      if (*IO_button_left & 0x80)
        yaw++;
      return yaw;
    }
    

    We now know something about the way the game tracks orientation and that it uses a reference frame where positive rotation is to the left. Based on the cross references, we can see that this function is called by the ship_command_yaw and ship_command_yaw_easy functions. Let's look at that first one since it's "easier":

    Yaw (Easy)

    ; Control the ship's yaw by adjusting the angle.
    ; In the three easy modes the player only controls the rotation angle of the ship
    ; with the rotate left and rotate right buttons.
    ; In the easiest mode the ship is limited to mostly upright angles.
    ;
    ; Yawing does cost a small amount of fuel and there is a tail call
    ; to yaw_drain_fuel.
    ;
    ship_command_yaw_easy:
    ship_command_yaw+b
    63d4
    2497
    ; Read the fuel variable
    63d6
    10fb
    ; If bit 8 is not set (no fuel) jump to the shared RTS
    63d8
    200464
    ; Read the rotation buttons
    63db
    f0f6
    ; If neither (or both) are set, no rotation so jump to the shared RTS
    63dd
    18
    CLC
    ; Clear carry flag
    63de
    6567
    ; A = Rotation (+1/-1) plus the high byte of the ship's angle
    63e0
    8567
    ; Store the updated rotation back into the high byte
    63e2
    aa
    TAX
    ; Cache the high byte in X
    63e3
    4a
    LSR
    ; A = A / 2
    63e4
    4a
    LSR
    ; A = A / 2
    63e5
    291f
    AND #$1f
    ; A = (Rotation / 4) % 32 -- how far is the angle into the quadrant
    63e7
    8502
    ; Store this remainder
    63e9
    a523
    ; What's the current difficulty level?
    63eb
    d011
    ; Non-zero difficulty allows arbitrary rotation angles
    63ed
    e8
    INX
    ; But level 0 prevents the ship from rotating past +/- 90 deg
    63ee
    f006
    ; if this is 0, the ship would have rotated too far
    63f0
    e042
    CPX #$42
    ;
    63f2
    d00a
    ;
    63f4
    a910
    LDA #$10
    ;
    cancel_rotation:
    63ee
    63f6
    8502
    ; Store the corrected remainder
    63f8
    0a
    ASL
    ;
    63f9
    0a
    ASL
    ;
    63fa
    8567
    ; And store the correct high byte for the ship's angle
    63fc
    90d5
    ; if no rotation happened do not drain any fuel, otherwise fall through into yaw_drain_fuel
    ; Wrapper that drains a small amount of fuel when yawing.
    ; All difficulties use this.
    ; There is a tail call to fuel_drain
    yaw_drain_fuel:
    ship_command_yaw+6cship_command_yaw_easy+17ship_command_yaw_easy+1e
    63fe
    a200
    LDX #$00
    ; Pass a 16-bit BCD value 0x0600 to
    6400
    a006
    LDY #$06
    ; the fuel drain wrapper
    6402
    d05d
    ; (Always taken)

    Note that the last instruction in the function is a BNE, even though a constant non-zero value has just been loaded into Y, so it becomes an always-taken relative jump. This is one byte shorter than the equivilant JMP instruction.

    rts_63d3:
    ship_command_yaw+3bship_command_yaw_easy+2ship_command_yaw_easy+7ship_command_yaw_easy+28
    63d3
    60
    RTS
    ; Shared RTS instruction used by several functions

    Another way that programmers saved memory was by reusing instructions across different functions. The "function" at 63d3 is a single RTS instruction that other nearby functions use instead of having their own RTS. This complicates the control-flow analysis of tools like ghidra and sometimes requires manual annotation to decompile.

    Yaw (Hard)

    ; Missions are selected with the "Game Select" button and range from 0 - 3:
    ;
    ; 0 "Training" Light gravity Friction Controlled Rotation
    ; 1 "Cadet" Moderate gravity No Friction Controlled Rotation
    ; 2 "Prime" Strong gravity No Friction Controlled Rotation
    ; 3 "Command" Moderate gravity No Friction Rotational Momentum
    ;
    mission_difficulty:
    GameLoop+23GameLoop+abfuel_drain_thrust+6ship_update+5fmission_button_handler+9mission_button_handler+bmission_button_handler+fship_reset+fship_command_yaw+5ship_command_yaw_easy+15
    0023
    .byte
    ; Mission difficulty 0 - 3

    ; ship_command_yaw handles the hard yaw mode in difficulty 3 "Command"
    ; where the player controls the yaw thrusters, rather than the angle.
    ; this allows the ship to rotate all the way around and they have to stop
    ; rotation by firing the opposite thruster. it's really hard!
    ;
    ; if the game is in a lower difficulty mission, then ship_command_yaw_easy will
    ; be called instead.
    ship_command_yaw:
    GameLoop+110
    633c
    a556
    ; Only process commands if ship_abort is not set
    633e
    f001
    ;
    6340
    60
    RTS
    ; Abort in process, nothing to do here
    not_aborting:
    633e
    6341
    a523
    ; Use the easy mode for yaw commands other than
    6343
    c903
    CMP #$03
    ; if mission_difficulty == 3 ("Command")
    6345
    f003
    ; then we will use yaw momentum that is much harder
    6347
    4cd463
    ; else Tail position call to ship_command_yaw_easy
    yaw_momentum:
    6345
    634a
    a566
    634c
    18
    CLC
    ; these are normal signed
    634d
    6503
    ; 16-bit values, so the
    634f
    8566
    ; code is much simpler than
    6351
    a567
    ; the signed magnitude stuff
    6353
    6504
    ; that comes later
    6355
    8567
    ; for the ship_update
    6357
    4a
    LSR
    ; Divide the high-byte of the angle
    6358
    4a
    LSR
    ; by four
    6359
    291f
    AND #$1f
    ; And mask it down to 0-31
    635b
    8502
    ; Store it in the 8-bit ship angle
    635d
    a000
    LDY #$00
    ;
    635f
    a503
    ;
    6361
    a604
    ;
    6363
    f00a
    ; if yaw_rate high == 0, then check low
    6365
    e8
    INX
    ; increment X
    6366
    d00b
    ; if yaw_rate high != 0xFF, then don't set yaw dir
    6368
    c9c0
    CMP #$c0
    ; if yaw_rate > -0x40
    636a
    9007
    ; then don't set yaw dir
    set_yaw_slow:
    6371
    636c
    88
    DEY
    ; decrement Y to mark that maybe we are close enough to stopped?
    636d
    3004
    ; always taken since Y now = -1
    yaw_rate_high_zero:
    6363
    636f
    c941
    CMP #$41
    ; if yaw_rate < 0x41
    6371
    90f9
    ; then jump to set_yaw_slow
    yaw_check_fuel:
    6366636a636d
    6373
    8405
    ; Set if yaw_rate is close enough to zero to be almost stopped
    6375
    2497
    ; Do we have any fuel left?
    6377
    105a
    ; If bit 7 is not set, we can't fire yaw thruster, so return
    6379
    200464
    ; We have some fuel, so see if the player has pressed yaw button
    637c
    0a
    ASL
    ; The result in A is +/- 1
    637d
    0a
    ASL
    ; Shift-left
    637e
    0a
    ASL
    ; four times
    637f
    0a
    ASL
    ; to result in +/-16
    6380
    a000
    LDY #$00
    ; Y is the high-byte for the yaw button command
    6382
    29f0
    AND #$f0
    ; Mask out the bottom bits of the yaw command (which should already be 0)
    6384
    f025
    ; If they are not pressing a button?
    6386
    9001
    ; If positive yaw command, then we're going left Y:A = +16
    6388
    88
    DEY
    ; Going right: Y:A = -16
    yaw_cmd_left:
    6386
    6389
    18
    CLC
    ; 16-bit add of Y:A and yaw_rate
    638a
    6503
    ; add the low bytes
    638c
    aa
    TAX
    ; cache the result low byte in X
    638d
    98
    TYA
    ; move the high byte of the yaw command into A
    638e
    6504
    ; add the high bytes
    6390
    100a
    ; if yaw_rate > 0 goto yaw_positive
    6392
    c9fc
    CMP #$fc
    ; if yaw_rate > -0x400
    6394
    b00e
    ; -- goto yaw_positive
    6396
    a9fc
    LDA #$fc
    ; cap yaw_rate at -0x400
    6398
    a200
    LDX #$00
    ; which is 0xFC00 as a 16-bit value
    639a
    f008
    ; always taken
    yaw_positive:
    6390
    639c
    c904
    CMP #$04
    ; if yaw_rate < +0x400
    639e
    9004
    ; -- goto yaw_positive
    63a0
    a903
    LDA #$03
    ; cap yaw_rate at +0x03e0
    63a2
    a2e0
    LDX #$e0
    ; which is 0x03e0 since it is positive
    store_yaw_rate:
    6394639a639e
    63a4
    8504
    ; store the high byte
    63a6
    8603
    ; store the low byte
    63a8
    4cfe63
    ; tail position call to spend some fuel for firing the yaw thrusters
    no_yaw_command:
    6384
    63ab
    a505
    ; if yaw_slow == 0 (are we slow enough?)
    63ad
    f01e
    ; then nothing to do
    63af
    a506
    ; is yaw_nonzero == 0
    63b1
    f005
    ;
    63b3
    a900
    LDA #$00
    ; Set the new yaw_rate to be 0
    63b5
    aa
    TAX
    ; high and low bytes in X:A == 0
    63b6
    f011
    ; always taken
    L63b8:
    63b1
    63b8
    a503
    ; Check for yaw_rate == 0
    63ba
    0504
    ; high and low bytes
    63bc
    f00f
    ; and update the yaw_nonzero value
    63be
    a950
    LDA #$50
    ; Decay the yaw rate to 0x0050
    63c0
    a200
    LDX #$00
    ; by setting X:A
    63c2
    2404
    ; if yaw_rate is positive
    63c4
    1003
    ; then use this value
    63c6
    a9b0
    LDA #$b0
    ; else set X:A to 0xFFB0
    63c8
    ca
    DEX
    ; (by decrement)
    update_yaw_rate:
    63b663c4
    63c9
    8604
    ; Store X:A into the yaw_rate
    63cb
    8503
    ; high and low bytes
    update_yaw_nonzero:
    63ad63bc
    63cd
    a503
    ; are any bits set in the yaw_rate
    63cf
    0504
    ; high or low bytes?
    63d1
    8506
    ; if so update yaw_nonzero

    yaw_rate:
    ship_reset+8ship_command_yaw+11ship_command_yaw+23ship_command_yaw+4eship_command_yaw+6aship_command_yaw+7cship_command_yaw+8fship_command_yaw+91
    0003
    .word
    ; 16-bit yaw rate for momentum mode
    yaw_slow:
    ship_command_yaw+37ship_command_yaw+6f
    0005
    .byte
    ; Is the yaw rate slow enough to decay
    yaw_nonzero:
    ResetGameState+aship_command_yaw+73ship_command_yaw+95
    0006
    .byte
    ; Is the yaw rate non-zero

    Ship state

    Most of the ship's state is stored in zeropage global variables. These include the XY acceleration (stored as 17-bit signed magnitude), the velocity and position (stored as 9-bit signed magnitude?)

    ship_accel_sign[0]:
    ship_compute_accel_xy+9ship_compute_accel_xy+cship_update+5b
    0046
    .byte[2]
    ; The sign bits for the X and Y accelerations
    ship_enable_x:
    ship_update+cS711c+2S736b+2
    0055
    .byte
    ; Disable physics for the X and Y axes (bit 7)
    ship_abort:
    RESET+12cRESET+148RESET+1d7RESET+24fRESET+251RESET+271GameLoop+197GameLoop+19bGameLoop+1ccGameLoop+1ffdebris_something+cdebris_something+55debris_something+84thrust_smoothing_maybe+4ship_command_yaw+0abort_procedure_update+2abort_procedure_update+39abort_procedure_update+4dmultiply+5abort_procedure_update+52
    0056
    .byte
    ; Has the abort button been triggered?
    ship_enable_y:
    GameLoop+1f1S711c+4S738a+2
    0057
    .byte
    ; Accessed via array from ship_enable_x
    ship_accel[0]:
    ship_compute_accel_xy+32ship_compute_accel_xy+3cship_update+57
    0058
    .byte[2]
    ; The X and Y acceleration magnitudes for the ship's thrust
    ship_vel_sign_x:
    bcd_score_maybe+29ship_update+24ship_update+47ship_update+78S711c+12landed_choose_random+17
    005a
    .byte
    ; Sign bit for the X velocity
    ; 0x8F == exploding?
    ; 0x00 == +50 points
    ; 0x4x == +15 points
    ; 0x0x == + 5 points (bottom bits ignored?)
    ; initialized to 0 by ResetGameState
    ship_state_maybe:
    InitGame+5GameLoop+aGameLoop+16dGameLoop+188GameLoop+19fGameLoop+1a4GameLoop+1d3fuel_lost_to_crash_wrapper+0ship_update+6GameRunningLoop+1d0GameRunningLoop+22eS711c+62explode_something_lots_math+49S711c+114S711c+116S711c+11fscore_landing_bonus+2score_landing_bonus+8S711c+119
    005d
    .byte
    ; Bit map of stuff about the ship
    ship_vel[0]:
    GameLoop+203ResetGameState+14ResetGameState+16ResetGameState+1agame_state_reinit_maybe+2aship_vel_decay+2ship_vel_decay+6ship_vel_decay+12ship_vel_decay+17ship_vel_decay+19ship_vel_decay+1dbcd_score_maybe+6bcd_score_maybe+8ship_update+10ship_update+14ship_update+49ship_update+4cship_update+7aship_update+7dabort_procedure_update+29abort_procedure_update+2dabort_procedure_update+31abort_procedure_update+43landed_choose_random+0landed_choose_random+dS711c+ffS711c+10dS736b+4S736b+cS738a+4S738a+c
    005e
    .word[2]
    ; Ship horizontal X and Y
    ship_pos[0]:
    game_state_reinit_maybe+2fship_update+28ship_update+2bship_update+34ship_update+36S711c+10S711c+3bS711c+c9S711c+cbS711c+d2S711c+d5S711c+d7S711c+daS711c+dcS711c+e0shuffle_7_to_9+0shuffle_7_to_9+2shuffle_7_to_9+bshuffle_7_to_9+dS711c+122S711c+1b8S711c+1baS711c+1dbS711c+1df
    0007
    .word[2]
    ; Ship position 16-bit

    mission_gravity[0]:
    ship_reset+11
    62a7
    .byte[4] 11, 11, 22, 11
    ; Gravity settings for the different missions
    mission_lamps[0]:
    ship_reset+16
    62e2
    .byte[4] 08, 04, 02, 01
    ; Bitmask of lamps to illuminate based on the mission difficulty

    ; Reset the ship but keep the angle the same
    ship_reset_saved_angle:
    abort_procedure_update+26
    62bc
    a502
    ; Load the current ship angle and fall through
    ; Reset the ship to a new angle and re-read the mission parameters
    ; A: Ship's starting angle from 0 - 31
    ship_reset:
    game_state_reinit_maybe+50
    62be
    0a
    ASL
    ; Multiply the small angle
    62bf
    0a
    ASL
    ; by four
    62c0
    8567
    ; and store that in the high byte of the ship angle
    62c2
    a900
    LDA #$00
    ; Memset the yaw_rate, yaw_slow and yaw_nonzero
    62c4
    a203
    LDX #$03
    ; parameters to 0
    ship_reset_bzero:
    62c9
    62c6
    9503
    STA yaw_rate,X
    ; bzzzzt
    62c8
    ca
    DEX
    ; x--
    62c9
    10fb
    ; keep bzeroing
    62cb
    8566
    ; zero the low byte of the ship angle
    62cd
    a623
    ; Read the current mission setting
    62cf
    bda762
    ; Read the mission specific gravity
    62d2
    8563
    ; and store it in a global
    62d4
    bde262
    ; Read the lamps to illuminate for this mission
    62d7
    aa
    TAX
    ; Move them to X
    62d8
    a9f0
    LDA #$f0
    ; Keep the rest of the bits on
    62da
    4c5f79
    ; make a tail call to turn on the lamps

    Ship Position and Velocity update

    The mult16 function is used to compute the ship's X and Y acceleration based on the current thrust, a thrust-to-force lookup table, and then multiplying by the sine and cosine of the ship's angle to rotate the force into the screen coordinate frame.

    sine_quadrants[0]:
    ship_compute_accel_xy+6
    76f2
    .byte[4] 00, 80, c0, 40
    ; Define the quadrants
    thrust_to_acc[0]:
    ship_compute_accel_xy+10
    76f6
    .byte[16] 00, 02, 05, 08, 0b, 0d, 0f, 10, 11, 12, 13, 14, 16, 18, 1a, 1c
    ; How much acceleration comes from the different thrust levels
    sine_table[0]:
    ship_compute_accel_xy+2aship_compute_accel_xy+36
    76e9
    .byte[9] 00, 32, 5f, 9a, b5, cd, ee, fb, ff
    ; Reduced sine table for 0-45 degrees

    ; Update the ship's acceleration in the screen reference frame
    ; Compute sin and cos of the ship's angle and multiplies the
    ; current thrust setting to get the X and Y acceleration.
    ; Also computes the sign bit for these accelerations
    ship_compute_accel_xy:
    GameLoop+10d
    6b32
    a502
    ; Read the reduced ship's angle (updated by ship_command_yaw_easy )
    6b34
    4a
    LSR
    ; This is 0-31, where 0 is horizontal facing right, 8 is vertical,
    6b35
    4a
    LSR
    ; 16 is horizontal facing left, 31 is straight down
    6b36
    4a
    LSR
    ; Divide this reduced angle by 8 to get the quadrant that it is in
    6b37
    aa
    TAX
    ;
    6b38
    bdf276
    ; Read the quadrant bits (bit 7 = X direction, bit 6 = Y direction)
    6b3b
    8546
    ; Store the X accleration sign bit (used by add16_signed_mag)
    6b3d
    0a
    ASL
    ; Shift it left once
    6b3e
    8547
    ; Store the Y acceleration sign bit
    6b40
    a601
    ;
    6b42
    bdf676
    ;
    6b45
    850b
    ;
    6b47
    a502
    ; Get the ship's angle (0-31)
    6b49
    290f
    AND #$0f
    ; Get how far from horizontal it is (up or down)
    6b4b
    c909
    CMP #$09
    ; If it is 0-8, use the first half of the sine table
    6b4d
    9004
    ; (
    6b4f
    490f
    EOR #$0f
    ; Invert the clamped angle, which flips it around 45 degrees
    6b51
    6900
    ADC #$00
    ; Why carry?
    flip_to_other_half:
    6b4d
    6b53
    8537
    ;
    6b55
    4907
    EOR #$07
    ;
    6b57
    6901
    ADC #$01
    ;
    6b59
    290f
    AND #$0f
    ;
    6b5b
    aa
    TAX
    ;
    6b5c
    bde976
    ;
    6b5f
    a40b
    ;
    6b61
    20ef70
    ;
    6b64
    8558
    ;
    6b66
    a637
    ;
    6b68
    bde976
    ;
    6b6b
    20ef70
    ;
    6b6e
    8559
    ;
    6b70
    60
    RTS
    ;

    Now that we have the math functions for computing signed magnitude addition and transforming the thrust vectors into the XY screen coordinate frame, we can finally update the ship's position.

    ; Update ship acceleration, velocity and position in the XY frame once per clock tick
    ;
    ; The 16-bit velocity is normally divided by 256, but if the screen is zoomed in
    ; then it is only divided by 64. Since the NMI runs at 250 HZ, this division
    ; effectively is the same as multiplying the velocity by dt.
    ;
    ; This updates the ship position on each axis
    ;
    ; x = x + vx * dt
    ; vx = vx + thrust_x * dt
    ; y = y + vy * dt
    ; vy = vy + (thrust_y - gravity) * dt
    ;
    ; add16_signed_mag_arg1 is used for position update since the position sign is always positive
    ; add16_signed_mag_core is used to accumulate the thrust_y - gravity plus velocity
    ;
    ship_update:
    GameLoop+139GameLoop+18c
    6c68
    a202
    LDX #$02
    ; Make two loops, one for X and one for Y. Note that i is decremented by 2 each time through the loop 6ce7
    ship_update_loop:
    6ceb
    6c6a
    8637
    ; Store the iterator temporary
    6c6c
    a900
    LDA #$00
    ; Initialize the global helper variables
    6c6e
    855d
    ;
    6c70
    854d
    ; Position sign is always positive
    6c72
    8549
    ; velocity_high = 0
    6c74
    b555
    ; is bit 7 set in ship_enable_x or ship_enable_y (depending on X)
    6c76
    3028
    ; if so skip the physics for this axis
    6c78
    b55f
    ; Copy the high byte of the velocity for this axis
    6c7a
    8548
    STA delta2
    ; into the low-byte of delta2
    6c7c
    b55e
    ; Load the low byte of the ship's velocity
    6c7e
    244e
    ; Check if we have zoomed in on the ship and terrain
    6c80
    700a
    ; Skip the zoom if we haven't zoomed in (bit 6 is not set)
    6c82
    0a
    ASL
    ; Double the low byte of the velocity
    6c83
    2648
    ROL delta2
    ; Double the high byte of the velocity (shifting in from the low byte)
    6c85
    2649
    ; Double the high high byte of the velocity
    6c87
    0a
    ASL
    ; And do it again...
    6c88
    2648
    ROL delta2
    ; This multiplies the velocity by four
    6c8a
    2649
    ; Note that the bottom byte is otherwise unused
    skip_zoom:
    6c80
    6c8c
    b55a
    ; Get the sign bit for the velocity for this axis
    6c8e
    854c
    ; and store it in the delta2 workspace
    6c90
    b508
    ; Get the position high byte for this axis
    6c92
    48
    PHA
    ; Push it
    6c93
    b507
    ; Get the position low byte
    6c95
    aa
    TAX
    ; move it into X
    6c96
    68
    PLA
    ; Pop the high byte so that the 16-bit position is in A:X
    6c97
    20006d
    ; Compute Position + Velocity for this axis
    6c9a
    a637
    ; Restore the iterator
    6c9c
    9508
    ; Write the high byte for this axis
    6c9e
    9407
    ; and write the low byte for this axis
    skip_physics:
    6c76
    6ca0
    a900
    LDA #$00
    ; Gravity always has a zero high-byte
    6ca2
    8549
    ;
    6ca4
    8a
    TXA
    ; Move the iterator to the accumulator for testing
    6ca5
    f002
    ; If i == 0, don't add any gravity (x axis)
    6ca7
    a563
    ; gravity global is updated based on the mission difficulty
    no_gravity:
    6ca5
    6ca9
    8548
    STA delta2
    ; Store either 0 or the gravity into the low-byte of delta2
    6cab
    a980
    LDA #$80
    ; Since gravity is always negative
    6cad
    854c
    ; set the sign bit for delta2 to negative
    6caf
    b45a
    ; Get the sign bit of this axis' velocity into Y
    6cb1
    b55f
    ; And load A:X with this axis' 16-bit velocity magnitutde
    6cb3
    48
    PHA
    ; (same logic as before
    6cb4
    b55e
    ; to push things onto the stack
    6cb6
    aa
    TAX
    ; and the pop them off again
    6cb7
    68
    PLA
    ; into the right registers)
    6cb8
    20fe6c
    ; Compute Velocity + Gravity for this axis
    6cbb
    a537
    ; Restore the iterator
    6cbd
    4a
    LSR
    ; Divide it by two
    6cbe
    aa
    TAX
    ; and move it back to X
    6cbf
    b558
    ; Get the 8-bit magnitude of the ship's thrust on this axis
    6cc1
    8548
    STA delta2
    ; and copy it into delta2
    6cc3
    b546
    ; Get the thrust sign bit
    6cc5
    854c
    ; and copy it into delta2
    6cc7
    a523
    ; Depending on the mission difficulty
    6cc9
    c902
    CMP #$02
    ; 2 == "Prime", which has strong gravity
    6ccb
    d00c
    ; other missions don't need to tweak the thrust
    6ccd
    a548
    LDA delta2
    ; Multiply the delta2 value by 1.5
    6ccf
    4a
    LSR
    ; through clever shift and addition
    6cd0
    18
    CLC
    ; tricks with the carry flags
    6cd1
    6548
    ADC delta2
    ; delta2 = delta2 + (delta2 / 2)
    6cd3
    8548
    STA delta2
    ; effectively
    6cd5
    9002
    ; no overflow
    6cd7
    e649
    ; add the overflow to the high byte
    add_thrust_to_vel:
    6ccb6cd5
    6cd9
    20046d
    ; delta1 already has Velocity + Gravity, so this is Velocity + Gravity + Thrust
    6cdc
    48
    PHA
    ; Result is in A:Y, push the high byte
    6cdd
    8a
    TXA
    ; Result sign bit is in X
    6cde
    a637
    ; Restore the iterator
    6ce0
    955a
    ; Update the velocity sign bit
    6ce2
    945e
    ; Update the velocity low byte
    6ce4
    68
    PLA
    ; Restore the high byte
    6ce5
    955f
    ; And store the high byte
    6ce7
    ca
    DEX
    ; Decrement i twice
    6ce8
    ca
    DEX
    ; since the loop is over 16-bit values
    6ce9
    3003
    ; If i is now negative (2 -> 0 -> -2), we're done
    6ceb
    4c6a6c
    ; If not make another pass through the loop
    ship_update_done:
    6ce9
    6cee
    60
    RTS
    ; Ship position and velocity updated. Return!

    Thrust and Fuel

    The player has a large lever that controls the thrust from the ship's engine. More thrust burns more fuel and the game rewards good landings with more fuel.

    Fuel

    The fuel is stored in BCD format, which means that each byte can represent up 00 - 99, so a three byte value can represented 000000 to 999999. The 6502 has a special mode in the ALU that causes addition and subtraction to produce results in this format. Games often used it for scores since they wanted to display a base-10 value for the player.

    ; The remaining fuel is stored as a 3-byte BCD amount,
    ; which gives a maximum of 6 digits in base 10.
    ; It is stored in reverse order, so fuel_tank[0] is the LSB and fuel_tank[2] is MSB.
    fuel_tank[0]:
    draw_out_of_fuel_screen+1cfuel_lost_to_crash+20fuel_lost_to_crash+24fuel_increase_limit_9999+6fuel_increase_limit_9999+afuel_increase_limit_9999+12fuel_increase_limit_9999+14fuel_drain+7fuel_drain+dfuel_drain+12fuel_drain+22fuel_drain+24fuel_drain+26fuel_drain+2cfuel_drain+2efuel_drain+30fuel_drain+32
    00ac
    .byte[3]
    ; BCD amount of fuel in the tank
    fuel_used[0]:
    fuel_lost_to_crash+4fuel_lost_to_crash+9fuel_lost_to_crash+2afuel_lost_to_crash+2cfuel_lost_to_crash+31fuel_lost_to_crash+33fuel_drain+3bfuel_drain+3f
    00a1
    .byte[3]
    ; BCD amount of fuel used during the mission
    ; Bitmask tracking the current fuel (and game) state
    ; 0x80 = Have fuel
    ; 0x40 = Out of fuel
    ; 0x00 = Not playing
    fuel_state:
    GameLoop+0GameLoop+8GameLoop+1abdraw_out_of_fuel_screen+18coin_inserted+5thrust_smoothing_maybe+42fuel_lost_to_crash+35abort_procedure_update+6fuel_increase_limit_9999+2ship_command_yaw+39fuel_drain+18fuel_drain+1eship_command_yaw_easy+0
    0097
    .byte
    ; Bitmask of fuel state

    Various functions in the code will spend fuel, and they either call fuel_drain_16 to drain a two-byte amount, or the full fuel_drain that takes a three-byte amount. This is in BCD and the A:X:Y calling convention is different from some other functions that work on multi-byte arguments.

    ; Drain a 16-bit BCD X:Y amount of fuel from the ship.
    ; This tail calls into fuel_drain
    fuel_drain_16:
    fuel_drain_thrust+1cyaw_drain_fuel+4
    6461
    a900
    LDA #$00
    ;
    ; Drain a 24-bit BCD A:X:Y amount of fuel from the ship
    ;
    fuel_drain:
    fuel_lost_to_crash+2e
    6463
    f8
    SED
    ; Enable Decimal mode since the fuel is in BCD
    6464
    8539
    ; Cache the MSB of the argument into GenBytes 39
    6466
    8638
    ; ... the middle byte into 38
    6468
    8437
    ; ... the lowest byte into 37
    646a
    a5ac
    ; Load LSB of fuel
    646c
    38
    SEC
    ; Clear the carry (to start a SBC chain)
    646d
    e537
    ; fuel_tank[0] - GenByte37
    646f
    a8
    TAY
    ; -> Y
    6470
    a5ad
    ; Load the middle byte of fuel
    6472
    e538
    ; fuel_tank[1] - GenByte38
    6474
    aa
    TAX
    ; -> X
    6475
    a5ae
    ; Load the MSB of fuel
    6477
    e539
    ; fuel_tank[2] - GenByte39 -> A
    6479
    b014
    ; If carry is not set, the remaining fule did not go below zero
    low_fuel:
    6497
    647b
    2497
    ; Read the fuel_state global
    647d
    1029
    ; If bit 7 is not set, then we've already run out of fuel
    647f
    a940
    LDA #$40
    ; Store 0x40 indicting out of fuel
    6481
    8597
    ; into fuel_state
    6483
    a900
    LDA #$00
    ; Write 00:00:00 into fuel_tank
    6485
    85ac
    ; no fuel
    6487
    85ad
    ; so sad
    6489
    85ae
    ; burma shave
    648b
    858d
    ; stop the clock?
    648d
    f00a
    ; always taken
    fuel_no_underflow:
    6479
    648f
    84ac
    ; Store LSB of the 3-byte remaining fuel
    6491
    86ad
    ; Store the middle byte
    6493
    85ae
    ; Store the MSB of fuel_tank back in the global
    6495
    05ad
    ; Are both the middle and MSB zero?
    6497
    f0e2
    ; If so we are in a low fuel state
    fuel_track_used:
    648d
    6499
    18
    CLC
    ; clear the carry to start an ADC chain
    649a
    a002
    LDY #$02
    ; for x = 0,1,2
    649c
    a200
    LDX #$00
    ; starting with the LSB
    fuel_used_loop:
    64a6
    649e
    b5a1
    ; add the amount of fuel drained
    64a0
    7537
    ; to the global fuel_used
    64a2
    95a1
    ; storing back in the global
    64a4
    e8
    INX
    ; x++
    64a5
    88
    DEY
    ; y--
    64a6
    10f6
    ; if y isn't negative, do the next byte
    fuel_drain_done:
    647d
    64a8
    d8
    CLD
    ; Reset to binary mode (no more BCD)
    64a9
    60
    RTS
    ; Return to the caller

    The yaw routines burn a little bit of fuel, but most of the burn comes from the main engine. This is handled here:

    ; While the main engine is thrusting, fuel is drained based on the lever position
    fuel_drain_thrust:
    GameLoop+113
    6b71
    a9da
    LDA #$da
    ; Default is to lose 218 fuel per thrust unit
    6b73
    a40b
    ; If thrust mode is negative
    6b75
    3008
    ; then use the default
    6b77
    a623
    ; Otherwise if the mission is
    6b79
    e002
    CPX #$02
    ; not equal to 2 ("Prime") with strong gravity
    6b7b
    d002
    ; then also use the default
    6b7d
    a990
    LDA #$90
    ; Mission 2 gets a little less burn per thrust unit
    fuel_thrust_multiply:
    6b756b7b
    6b7f
    20ef70
    ; A = thrust cost Y = thrust setting
    6b82
    aa
    TAX
    ; Ignores the low bits, uses just the high byte of the result
    6b83
    a000
    LDY #$00
    ; Only use the bits in X
    6b85
    8437
    ; So store 0 in gb37
    6b87
    a007
    LDY #$07
    ; 8 digits requested
    6b89
    20c679
    ; Converts the binary result to BCD
    6b8c
    a8
    TAY
    ; Return is in Y:X:A, but fuel_drain_16 wants X:Y
    6b8d
    4c6164
    ; drains that much fuel (tail call)
    ; When the ship crashes, a random amount of fuel is lost
    fuel_lost_to_crash_wrapper:
    GameLoop+1e1
    6b90
    a55d
    ; If any of the bottom bits in ship_state are set
    6b92
    290f
    AND #$0f
    ; (bottom bits mean exploding?)
    6b94
    f049
    ; then return immediately
    fuel_lost_to_crash:
    S711c+138
    6b96
    f8
    SED
    ;
    6b97
    a59e
    ;
    6b99
    38
    SEC
    ;
    6b9a
    e5a2
    ;
    6b9c
    aa
    TAX
    ;
    6b9d
    a59f
    ;
    6b9f
    e5a3
    ;
    6ba1
    a8
    TAY
    ;
    6ba2
    a5a0
    ;
    6ba4
    e900
    SBC #$00
    ;
    6ba6
    d8
    CLD
    ;
    6ba7
    9036
    ;
    6ba9
    f004
    ;
    6bab
    a299
    LDX #$99
    ;
    6bad
    a099
    LDY #$99
    ;
    L6baf:
    6ba9
    6baf
    8a
    TXA
    ;
    6bb0
    d003
    ;
    6bb2
    98
    TYA
    ;
    6bb3
    f02a
    ;
    L6bb5:
    6bb0
    6bb5
    98
    TYA
    ;
    6bb6
    a4ad
    ;
    6bb8
    843a
    ;
    6bba
    a4ae
    ;
    6bbc
    843b
    ;
    6bbe
    a000
    LDY #$00
    ;
    6bc0
    84a2
    ;
    6bc2
    84a3
    ;
    6bc4
    206364
    ;
    6bc7
    a5a2
    ;
    6bc9
    a6a3
    ;
    6bcb
    2497
    ;
    6bcd
    3004
    ;
    6bcf
    a53a
    ;
    6bd1
    a63b
    ;
    L6bd3:
    6bcd
    6bd3
    8595
    ;
    6bd5
    8696
    STX Z96
    ;
    6bd7
    0596
    ORA Z96
    ;
    6bd9
    f004
    ;
    6bdb
    a97f
    LDA #$7f
    ;
    6bdd
    8590
    ;
    ; Another shared RTS
    rts_6bdf:
    fuel_lost_to_crash_wrapper+4fuel_lost_to_crash+11fuel_lost_to_crash+1dfuel_lost_to_crash+43
    6bdf
    60
    RTS
    ; Shared with a few nearby functions

    Thrust

    thrust_value:
    ship_compute_accel_xy+13ship_compute_accel_xy+2dfuel_drain_thrust+2
    000b
    .byte
    ; one of the 16 levels of thrust

    thrust_smoothing_maybe:
    GameLoop+104
    6414
    2422
    ;
    6416
    5004
    ;
    6418
    a556
    ;
    641a
    d044
    ;
    L641c:
    6416
    641c
    a584
    ;
    641e
    4a
    LSR
    ;
    641f
    4a
    LSR
    ;
    6420
    aa
    TAX
    ;
    6421
    4a
    LSR
    ;
    6422
    8537
    ;
    6424
    a000
    LDY #$00
    ;
    6426
    e482
    ;
    6428
    b01a
    ;
    642a
    a584
    ;
    642c
    38
    SEC
    ;
    642d
    e582
    ;
    642f
    a00f
    LDY #$0f
    ;
    6431
    9011
    ;
    6433
    c537
    ;
    6435
    900d
    ;
    6437
    a582
    ;
    6439
    a484
    ;
    643b
    20c070
    ;
    643e
    8a
    TXA
    ;
    643f
    4a
    LSR
    ;
    6440
    4a
    LSR
    ;
    6441
    4a
    LSR
    ;
    6442
    4a
    LSR
    ;
    6443
    a8
    TAY
    ;
    L6444:
    642864316435
    6444
    8401
    ;
    6446
    a000
    LDY #$00
    ;
    6448
    a585
    ;
    644a
    c94b
    CMP #$4b
    ;
    644c
    9008
    ;
    644e
    e683
    ;
    6450
    c684
    ;
    6452
    c684
    ;
    6454
    8485
    ;
    L6456:
    644c
    6456
    2497
    ;
    6458
    1004
    ;
    645a
    a522
    ;
    645c
    d002
    ;
    L645e:
    6458
    645e
    8401
    ;
    L6460:
    641a645c
    6460
    60
    RTS
    ; Return to caller

    Vector Generator

    This is what makes the Atari arcade games from this era so special -- the vectors instead of pixels! The actual drawing is done by specialized hardware, which is well documented in Jed Margolin's "Secret Life of Vector Generators" and come in two varieties: analog and digital vector generators. Lunar Lander has the DVG, which is built out of DACs that steer the CRT's electron beam around.

    The DVG implements its own little programming language with scaling, subroutines, and brightness control. The Hitch-Hacker's guide to the Atari DVG by Philip Pemberton has a good description of how they work, some of which is excerpted here. The main features of the DVG are:

  • 12-bit program counter
  • 4 level stack
  • Vector timer
  • 12-bit multipliers
  • 4-bit (16 level) brightness control
  • 1024x1024 resolution (10-bit DACs)

    Commands are 16-bits long, with the exception of VCTR and LBAS. The first nibble is the opcode so it is easy to visually tell what is going on.

    The main commands that are used in Lunar Lander are:

  • VCTR (0x0 - 0x9, 32-bit) Draw long vector from the current position to the new XY position
  • LABS (0xA, 32-bit) Move the beam to the new XY position and set global scale factor
  • HALT (0xB) Halt the generator and blank the screen
  • JSRL (0xC) Jump to a subroutine (stack is only 4-levels deep)
  • RTSL (0xD) Return from subroutine
  • JMPL (0xE) Jump to an address
  • SVEC (0xF) Draw a short vector from the current location to a new relative XY position

    The vector generator shares memory with the 6502. It has RAM from 0x4000 - 0x47FF and two ROM's from 0x4800 - 0x4FFF and 0x5000 - 0x5FFF (in the 6502's address space).

    Once the 6502 has written a "frame" to the vector generator's RAM, it drives IO_DMAGO which tells the vector generator to start executing from the RAM until it hits a HLT opcode.

    IO_DMAGO:
    GameLoop+ecvecgen_go_wait+0
    3000
    .byte
    ; Active low output for starting the Digital Vector Generator
    VecRamPtr:
    RESET+bdRESET+d3RESET+daRESET+e7RESET+edRESET+f9RESET+107RESET+1bcRESET+1c0RESET+1e6RESET+21bRESET+294RESET+296RESET+29bRESET+29dVecRamPtr_reset_0x4000+6GameLoop+69GameLoop+8fDrawString+15DrawString+1bDrawString+24DrawString+26vecram_memcpy_vecptr+5vecram_memcpy_vecptr+cvecram_memcpy_vecptr+edraw_lots_of_stuff+12draw_lots_of_stuff+39draw_lots_of_stuff+3fdraw_lots_of_stuff+48draw_lots_of_stuff+4cdraw_lots_of_stuff+52draw_lots_of_stuff+57draw_lots_of_stuff+acdraw_lots_of_stuff+b1draw_lots_of_stuff+e8draw_lots_of_stuff+10edraw_lots_of_stuff+113draw_lots_of_stuff+12dvec_draw_51ba_table+24draw_ship_prep_maybe+eDrawShip+4DrawShip+17DrawShip+1cDrawShip+23DrawShip+2aDrawShip+2dDrawShip+31debris_something+5edebris_something+64debris_something+6edebris_something+79draw_bonus_maybe+26SetDigitVecPtr+13SetDigitVecPtr+19WriteText_wrapper+aVecRomJSRL+8VecPtrUpdate_dupe+2VecPtrUpdate_dupe+4vec_copy_and_invert_maybe+bvec_copy_and_invert_maybe+16vec_copy_and_invert_maybe+1evec_copy_and_invert_maybe+27CalcDebrisPos+71CalcDebrisPos+7aCalcDebrisPos+7fCalcDebrisPos+8bVecPtrUpdate+2VecPtrUpdate+4
    0027
    .word
    ; Address of the end of the current commands for the DVG (starts at 0x4000)

    Fonts and number drawing

    ; Draw a BCD number without a leading zero
    ; A address of number on zero page
    ; Y number of digits
    DrawNumber_no_leading_zero:
    RESET+237RESET+249DrawNumber_3+2
    7b59
    38
    SEC
    ; Set carry flag and fall through into DrawNumber
    ; Draw a BCD number from the zero page
    ; A address of number on zero page
    ; Y number of digits
    ; C include leading zeros (carry flag clear)
    DrawNumber:
    DrawNumber_decimal_1+cdraw_even_more_stuff+11DrawNumber_and_copy+1
    7b5a
    08
    PHP
    ; Push the status register onto the stack (save the carry flag)
    7b5b
    88
    DEY
    ; decrement the number of digits (to compute last address)
    7b5c
    8438
    ; store number of digits in genbyte
    7b5e
    18
    CLC
    ; Clear the carry
    7b5f
    6538
    ; Add A to Y-1, which is the address of the MSB of the BCD digit
    7b61
    28
    PLP
    ; Restore the carry flag from the stack
    7b62
    aa
    TAX
    ; Move the starting address for the MSB digit into X
    DrawNumStringLoop:
    7b81
    7b63
    08
    PHP
    ; Push the carry flag again
    7b64
    8637
    ; Store the address into genbyte
    7b66
    b500
    LDA state_00,X
    ; Treat the zero page as an array and read the BCD digit
    7b68
    4a
    LSR
    ; Shift
    7b69
    4a
    LSR
    ; it
    7b6a
    4a
    LSR
    ; right
    7b6b
    4a
    LSR
    ; four times to get the upper digit from the top nibble into A
    7b6c
    28
    PLP
    ; Restore the carry flag
    7b6d
    201878
    ; Draw the upper digit
    7b70
    a538
    ; Do we do more digits?
    7b72
    d001
    ; if so do the lower digit for this value
    7b74
    18
    CLC
    ; Clear the carry (we will always do zeros now)
    DoLowerDigit:
    7b72
    7b75
    a637
    ; Get the current digit address
    7b77
    b500
    LDA state_00,X
    ; Zero-page array read for the digit
    7b79
    201878
    ; Draw the lower digit
    7b7c
    a637
    ; Decrement the digit address
    7b7e
    ca
    DEX
    ; to move to the next digit
    7b7f
    c638
    ; Decrement our digit counter
    7b81
    10e0
    ; If still positive, draw more digits
    7b83
    60
    RTS
    ; And we're done!

    ; Draw a single digit
    ; This will skip leading zeros when outputing numbers.
    ; A bottom four bits are the BCD digit to draw
    ; C include leading zeros if clear
    SetDigitVecPtr:
    DrawNumber+13DrawNumber+1f
    7818
    9004
    ; If carry is clear, always display the digit
    781a
    290f
    AND #$0f
    ; If not, check to see if the bottom nibble is zero
    781c
    f005
    ; If digit is zero, then draw character 0 which is a blank
    ChkSetDigitPntr:
    7818
    781e
    290f
    AND #$0f
    ; Mask out the top nibble
    7820
    18
    CLC
    ; Clear carry
    7821
    6901
    ADC #$01
    ; Add one to the digit, which shifts it to the correct font location
    DisplayDigit:
    781c
    7823
    08
    PHP
    ; Push the CPU state (preserve the carry flag?)
    7824
    0a
    ASL
    ; Multiple the digit by 2, since the DVG uses 16-bit words
    7825
    a000
    LDY #$00
    ; Y is the offset for the vector ram pointer
    7827
    aa
    TAX
    ; X = A
    7828
    bda257
    ; Read the low-byte of the font command
    782b
    9127
    STA (VecRamPtr),Y
    ; Store it in the vector pointer at the current location
    782d
    bda357
    ; Read the high-byte of the font command
    7830
    c8
    INY
    ; Increment Y
    7831
    9127
    STA (VecRamPtr),Y
    ; Store the high byte at the next location
    7833
    203878
    ; Add Y to the vector ram pointer
    7836
    28
    PLP
    ; Restore the carry flag
    7837
    60
    RTS
    ; Return to the caller
    ; Increment the vector ram command address
    ; Y number of bytes minus one added to the vector command list
    VecPtrUpdate:
    debris_something+7bSetDigitVecPtr+1bvec_copy_and_invert_maybe+29CalcDebrisPos+8d
    7838
    98
    TYA
    ; Move the number of bytes to A
    7839
    38
    SEC
    ; Set the carry, which will add the one extra
    783a
    6527
    ; Add Y+1 to the pointer
    783c
    8527
    ; Store it back in the pointer
    783e
    9002
    ; If no overflow skip the increment
    7840
    e628
    ; Overflowed, so increment the high byte
    VecPtr_no_overflow:
    783e
    7842
    60
    RTS
    ; Return to the caller

    The font table is stored in the vector generator's ROM; note that all of their addresses are offset by 0x4000 since it is mapped into the 6502 at 0x4000 but at 0x0000 in the DVG, and that all of the addresses are *words*, not bytes, so the CADF command is a subroutine call to word 0xADF, DVG address 0x15be, or 6502 address 0x55be

    ; The characters are stored in the order ' ', 0 - 9, A - Z
    CharPtrTbl[0]:
    DrawString+12DrawString+18SetDigitVecPtr+10SetDigitVecPtr+15
    57a2
    .word[47] cb93, cb44, cb95, cb99, cba1, cba8, cbaf, cbb6, cbbd, cbc2, cbca, cadf, cae7, caf4, cafa, cb02, cb0a, cb11, cb1a, cb21, cb28, cb2e, cb34, cb39, cb3f, cb44, cb4a, cb51, cb5a, cb62, cb69, cb6f, cb75, cb7a, cb81, cb86, cb8d, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 0000, 3700
    ; Subroutine calls for each font character

    Using our emulator for the DVG, we can render out this font table as an SVG:

    SVG rendering of the font

    Each character is it's own DVG subroutine. For example, here is the routine that draws A -- you can see that it consists of a few short vectors (command F) and then a RTSL (command D) to return:

    font_a[0]:

    55be
    .word[8] fac0, f2c2, f6c2, fec0, f906, f8c2, f602, d000
    ; Character "A"

    Ships

    twenty_subroutines[0]:

    53ee
    .word[20] c98f, c998, c99e, c9a7, c9b1, c9bb, c9c2, c9c9, c9d3, c9d7, c9db, c9e1, c9e5, c9e9, c9ef, c9f3, c98f, c998, c99e, c9a7
    ; Twenty subroutine calls, maybe ships or stars?

    Displays

    Some of the displays are also generated this way.

    which calls these subroutines:

    setup_font[0]:

    55b2
    .word[6] f201, f070, f200, f070, fe01, d000
    ; ???
    setup_display_2[0]:

    5576
    .word[8] 5140, 0240, f0cf, f3c3, 3680, c000, f3c7, d000
    ; ???
    setup_display_3[0]:

    5586
    .word[9] 4640, 0300, f3c8, f7c7, f8c3, f3c7, 44c0, 0700, d000
    ; ???
    setup_display_4[0]:

    5598
    .word[9] 40c0, 0300, f7c8, f3c7, f8c3, f7c7, 4240, 0700, d000
    ; ???

    Strings

    Text strings are drawn as a sequence of DVG subroutine calls, each character is a DVG JSRL subroutine call copied from the CharPtrTbl to the vector generator RAM. The strings are stored not in ASCII, but with the offsets into the font table of the character subroutine call, and the last character in the string has the high bit set as a terminator.

    draw_string_ptr:
    RESET+15aRESET+171DrawString+2DrawString+8DrawString+2eDrawString+30draw_lots_of_stuff+1edraw_lots_of_stuff+cddraw_lots_of_stuff+cfdraw_lots_of_stuff+d9draw_lots_of_stuff+145draw_bonus_maybe+28draw_bonus_maybe+4fdraw_bonus_maybe+53GameRunningLoop+7aGameRunningLoop+7fGameRunningLoop+87GameRunningLoop+90GameRunningLoop+98GameRunningLoop+b3GameRunningLoop+d2GameRunningLoop+dcGameRunningLoop+e0GameRunningLoop+fcGameRunningLoop+105GameRunningLoop+10bGameRunningLoop+11dGameRunningLoop+127GameRunningLoop+12cVecRomJSRL+dVecRomJSRL+11
    002b
    .word
    ; Pointer to character in a string being copied to vector ram

    ; DrawString
    ; X:Y Pointer to the string to write, terminated with 0x80 on the last character
    ; Copies the font subroutines to VecRamPtr
    ; Sets draw_string_ptr to point to the end of the string
    DrawString:
    RESET+1d2draw_lots_of_stuff+140WriteText+4c
    79f2
    862c
    ; Store the pointer high
    79f4
    842b
    ; and low bytes
    79f6
    a900
    LDA #$00
    ; for i = 0 ... strlen * 2
    copy_next_vec_instruction:
    7a13
    79f8
    4a
    LSR
    ; halve i since we're copying 16-bit JSRL vector subroutine
    79f9
    a8
    TAY
    ; calls for each letter and it is going up by two each byte
    79fa
    b12b
    ; Read low byte from the string
    79fc
    8539
    ; Cache it in gen byte
    79fe
    297f
    AND #$7f
    ; Strings are terminated by setting the high bit
    7a00
    aa
    TAX
    ; so strip the high bit from the letter
    7a01
    98
    TYA
    ; move y back into a
    7a02
    0a
    ASL
    ; and double it back to by index by words
    7a03
    a8
    TAY
    ; and back into y (what a dance)
    7a04
    bda257
    ; Index into the font table to get the low byte
    7a07
    9127
    STA (VecRamPtr),Y
    ; and store it in the vector ram ptr
    7a09
    c8
    INY
    ; next byte...
    7a0a
    bda357
    ; store the high byte of the font into the vector ram ptr
    7a0d
    9127
    STA (VecRamPtr),Y
    ; indexed by y
    7a0f
    c8
    INY
    ; and increment y again since we moved two bytes
    7a10
    98
    TYA
    ; and back into a
    7a11
    2439
    ; Test the cached version of the letter
    7a13
    10e3
    ; If positive, keep copying
    7a15
    18
    CLC
    ; Clear carry
    7a16
    6527
    ; Add number of bytes written to vector ram
    7a18
    8527
    ; to the VecRamPtr
    7a1a
    9002
    ; Did the low byte overflow?
    7a1c
    e628
    ; if so increment the high byte as well
    increment_char_ptr:
    7a1a
    7a1e
    98
    TYA
    ; Copy number of bytes copied to vector ram back to A
    7a1f
    4a
    LSR
    ; Divide it by two to get the number of characters in the string
    7a20
    652b
    ; Increase the character pointer
    7a22
    852b
    ; so it points to the end of the string
    7a24
    9002
    ; did the low byte overflow?
    7a26
    e62c
    ; if so increment the high byte as well
    draw_string_return:
    7a24
    7a28
    60
    RTS
    ; return to the caller

    The fixed strings are written to the screen by the WriteText function, which also handles localization and some sort of fixup that I haven't figured out yet:

    ; Write a localized string to the screen
    ; A String id
    WriteText:
    draw_insert_coin_screen+cdraw_insert_coin_screen+11draw_level_over_screen+26draw_push_start_screen+1ddraw_bonus_maybe+6edraw_lost_fuel_units_message+2draw_lost_fuel_units_message+7
    7a4f
    c918
    CMP #$18
    ; If id < 24
    7a51
    9003
    ; then this is a localized string (display strings start at 24 == "X")
    7a53
    0a
    ASL
    ; multiply id by two since we need a word address
    7a54
    d048
    ;
    write_localized_string:
    7a51
    7a56
    48
    PHA
    ; Cache the string id
    7a57
    a621
    ; Read the current language (set by dip switches)
    7a59
    f010
    ; If language is zero (english) handle it specially
    7a5b
    ca
    DEX
    ; Starting index into the localized string table
    7a5c
    7da15f
    ; based on the language setting - 1
    7a5f
    aa
    TAX
    ; Strings in the localized table
    7a60
    bda45f
    ; ???
    7a63
    a200
    LDX #$00
    ;
    7a65
    0a
    ASL
    ;
    7a66
    900f
    ;
    7a68
    ca
    DEX
    ;
    7a69
    b00c
    ;
    write_language_zero:
    7a59
    7a6b
    e908
    SBC #$08
    ; There's some fixups, not sure what is going on here
    7a6d
    c90c
    CMP #$0c
    ;
    7a6f
    b013
    ;
    7a71
    aa
    TAX
    ;
    7a72
    bd6d69
    ;
    7a75
    a200
    LDX #$00
    ;
    L7a77:
    7a667a69
    7a77
    18
    CLC
    ;
    7a78
    a002
    LDY #$02
    ;
    7a7a
    712f
    ; Maybe special characters? I'm really not sure yet.
    7a7c
    912f
    ;
    7a7e
    8a
    TXA
    ;
    7a7f
    c8
    INY
    ;
    7a80
    712f
    ;
    7a82
    912f
    ;
    L7a84:
    7a6f
    7a84
    68
    PLA
    ; Restore original string id from the stack
    7a85
    0a
    ASL
    ; Double it to get a word offset
    7a86
    c930
    CMP #$30
    ; Check if the original id is >= 24
    7a88
    b014
    ; this is a non-localized string (24 == "X")
    7a8a
    a621
    ; If the language is zero
    7a8c
    f010
    ; then this is also un-localized
    7a8e
    ca
    DEX
    ; Get the starting
    7a8f
    18
    CLC
    ; string id in the big localized string table
    7a90
    7d0058
    ; for this string language and add it to the string id
    7a93
    aa
    TAX
    ; Read A:Y = string_table_localized[string_id + string_table_lang_offset[language-1]]
    7a94
    bc0458
    ; low byte
    7a97
    bd0558
    ; and high bytes fo the pointer
    call_draw_string:
    7aa5
    7a9a
    aa
    TAX
    ; Convert the string pointer from A:Y into X:Y
    7a9b
    4cf279
    ; and tall call DrawString
    write_unlocalized_string:
    7a547a887a8c
    7a9e
    aa
    TAX
    ; Index into the english string table
    7a9f
    bc2b69
    ; and read the low
    7aa2
    bd2c69
    ; and high bytes into A:Y
    7aa5
    d0f3
    ;
    ; Shared return instruction for a few functions
    rts_7aa7:
    WriteText_wrapper+19
    7aa7
    60
    RTS
    ; Return to caller

    English strings

    There are 33 strings in the game and they are all indexed by number. For English the table has the pointers, note that string number 17 is an index into string 16 to reuse the word DESTROYED.

    string_table[0]:
    WriteText+50WriteText+53
    692b
    ; Pointers to the English strings
    str_PUSH_START[0]:
    692b
    69ab
    .byte[10] 34, 3e, 3a, 24, 00, 3a, 3c, 16, 38, bc
    ; 0 "PUSH START"
    str_LOW_ON_FUEL[0]:
    692b
    6979
    .byte[11] 2c, 32, 42, 00, 32, 30, 00, 20, 3e, 1e, ac
    ; 1 "LOW ON FUEL"
    str_OUT_OF_FUEL[0]:
    692b
    6984
    .byte[11] 32, 3e, 3c, 00, 32, 20, 00, 20, 3e, 1e, ac
    ; 2 "OUT OF FUEL"
    str_LOST[0]:
    692b
    698f
    .byte[4] 2c, 32, 3a, bc
    ; 3 "LOST"
    str_INSERT_COINS[0]:
    692b
    699f
    .byte[12] 26, 30, 3a, 1e, 38, 3c, 00, 1a, 32, 26, 30, ba
    ; 4 "INSERT COINS"
    str_PER_COIN[0]:
    692b
    69dd
    .byte[8] 34, 1e, 38, 00, 1a, 32, 26, b0
    ; 5 "PER COIN"
    str_AUXILIARY_FUEL_TANKS_DESTROYED[0]:
    692b
    69e6
    .byte[30] 16, 3e, 44, 26, 2c, 26, 16, 38, 46, 00, 20, 3e, 1e, 2c, 00, 3c, 16, 30, 2a, 3a, 00, 1c, 1e, 3a, 3c, 38, 32, 46, 1e, 9c
    ; 6 "AUXILIARY FUEL TANKS DESTROYED"
    str_CONGRATULATIONS[0]:
    692b
    6a04
    .byte[15] 1a, 32, 30, 22, 38, 16, 3c, 3e, 2c, 16, 3c, 26, 32, 30, ba
    ; 7 "CONGRATULATIONS"
    str_YOU_LANDED_HARD[0]:
    692b
    6a13
    .byte[15] 46, 32, 3e, 00, 2c, 16, 30, 1c, 1e, 1c, 00, 24, 16, 38, 9c
    ; 8 "YOU LANDED HARD"
    str_THAT_WAS_A_GREAT_LANDING[0]:
    692b
    6a22
    .byte[24] 3c, 24, 16, 3c, 00, 42, 16, 3a, 00, 16, 00, 22, 38, 1e, 16, 3c, 00, 2c, 16, 30, 1c, 26, 30, a2
    ; 9 "THAT WAS A GREAT LANDING"
    str_THE_EAGLE_HAS_LANDED[0]:
    692b
    6a3a
    .byte[20] 3c, 24, 1e, 00, 1e, 16, 22, 2c, 1e, 00, 24, 16, 3a, 00, 2c, 16, 30, 1c, 1e, 9c
    ; 10 "THE EAGLE HAS LANDED"
    str_THE_COLUMBIA_HAS_LANDED[0]:
    692b
    6a4e
    .byte[23] 3c, 24, 1e, 00, 1a, 32, 2c, 3e, 2e, 18, 26, 16, 00, 24, 16, 3a, 00, 2c, 16, 30, 1c, 1e, 9c
    ; 11 "THE COLUMBIA HAS LANDED"
    str_YOU_HAVE_LANDED[0]:
    692b
    6a65
    .byte[15] 46, 32, 3e, 00, 24, 16, 40, 1e, 00, 2c, 16, 30, 1c, 1e, 9c
    ; 12 "YOU HAVE LANDED"
    str_LIFE_SUPPORT_IS_GONE[0]:
    692b
    6a74
    .byte[20] 2c, 26, 20, 1e, 00, 3a, 3e, 34, 34, 32, 38, 3c, 00, 26, 3a, 00, 22, 32, 30, 9e
    ; 13 "LIFE SUPPORT IS GONE"
    str_YOUR_TRIP_IS_ONE_WAY[0]:
    692b
    6a88
    .byte[20] 46, 32, 3e, 38, 00, 3c, 38, 26, 34, 00, 26, 3a, 00, 32, 30, 1e, 00, 42, 16, c6
    ; 14 "YOUR TRIP IS ONE WAY"
    str_YOU_ARE_HOPELESSLY_MAROONED[0]:
    692b
    6a9c
    .byte[27] 46, 32, 3e, 00, 16, 38, 1e, 00, 24, 32, 34, 1e, 2c, 1e, 3a, 3a, 2c, 46, 00, 2e, 16, 38, 32, 32, 30, 1e, 9c
    ; 15 "YOU ARE HOPELESSLY MAROONED"
    str_COMMUNICATION_SYSTEM_DESTROYED[0]:
    692b
    6ab7
    .byte[30] 1a, 32, 2e, 2e, 3e, 30, 26, 1a, 16, 3c, 26, 32, 30, 00, 3a, 46, 3a, 3c, 1e, 2e, 00, 1c, 1e, 3a, 3c, 38, 32, 46, 1e, 9c
    ; 16 "COMMUNICATION SYSTEM DESTROYED"
    str_YOU_CREATED_A_TWO_MILE_CRATER[0]:
    692b
    6ad5
    .byte[29] 46, 32, 3e, 00, 1a, 38, 1e, 16, 3c, 1e, 1c, 00, 16, 00, 3c, 42, 32, 00, 2e, 26, 2c, 1e, 00, 1a, 38, 16, 3c, 1e, b8
    ; 18 "YOU CREATED A TWO MILE CRATER"
    str_YOU_JUST_DESTROYED_A_100_MEGABUCK_LANDER[0]:
    692b
    6af2
    .byte[40] 46, 32, 3e, 00, 28, 3e, 3a, 3c, 00, 1c, 1e, 3a, 3c, 38, 32, 46, 1e, 1c, 00, 16, 00, 04, 02, 02, 00, 2e, 1e, 22, 16, 18, 3e, 1a, 2a, 00, 2c, 16, 30, 1c, 1e, b8
    ; 19 "YOU JUST DESTROYED A 100 MEGABUCK LANDER"
    str_THERE_WERE_NO_SURVIVORS[0]:
    692b
    6b1a
    .byte[23] 3c, 24, 1e, 38, 1e, 00, 42, 1e, 38, 1e, 00, 30, 32, 00, 3a, 3e, 38, 40, 26, 40, 32, 38, ba
    ; 20 "THERE WERE NO SURVIVORS"
    str__POINTS[0]:
    692b
    69c2
    .byte[7] 00, 34, 32, 26, 30, 3c, ba
    ; 21 " POINTS"
    str_SELECT_OPTION[0]:
    692b
    69b5
    .byte[13] 3a, 1e, 2c, 1e, 1a, 3c, 00, 32, 34, 3c, 26, 32, b0
    ; 22 "SELECT OPTION"
    str__FUEL_UNITS_[0]:
    692b
    6993
    .byte[12] 00, 20, 3e, 1e, 2c, 00, 3e, 30, 26, 3c, 3a, 80
    ; 23 " FUEL UNITS "
    str_X[0]:
    692b
    69e5
    .byte c4
    ; 24 "X"
    str_450[0]:
    692b
    69c9
    .byte[3] 0a, 0c, 82
    ; 25 "450"
    str_600[0]:
    692b
    69cc
    .byte[3] 0e, 02, 82
    ; 26 "600"
    str_750[0]:
    692b
    69cf
    .byte[3] 10, 0c, 82
    ; 27 "750"
    str_900[0]:
    692b
    69d2
    .byte[3] 14, 02, 82
    ; 28 "900"
    str_1100[0]:
    692b
    64ff
    .byte[4] 04, 04, 02, 82
    ; 29 "1100"
    str_1300[0]:
    692b
    6503
    .byte[4] 04, 08, 02, 82
    ; 30 "1300"
    str_1550[0]:
    692b
    69d5
    .byte[4] 04, 0c, 0c, 82
    ; 31 "1550"
    str_1800[0]:
    692b
    69d9
    .byte[4] 04, 12, 02, 82
    ; 32 "1800"

    Localized strings

    localized_language_offset[0]:
    WriteText+d
    5fa1
    .byte[3] 00, 1d, 3a
    ; String id mapping for each language
    localized_string_offset[0]:
    WriteText+11
    5fa4
    .byte[93] eb, ee, ee, f7, e5, fd, df, 06, d0, 06, 12, 09, 15, f7, 06, 27, fa, 63, f4, 42, 2a, 00, 03, e5, 00, 00, 00, 00, 00, fa, f1, f4, f4, fa, fd, d6, 03, dc, 09, 06, 00, 12, f7, 06, eb, f7, 5d, 0c, 63, 36, 00, 03, d9, 00, 00, 00, 00, 00, df, e8, e5, eb, fa, e5, fd, 0c, eb, e5, 12, 09, 15, e8, 1e, f7, 00, 5d, ee, 03, 3f, 00, 0c, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 6b
    ; Index into the big string table for localized strings

    string_table_language_offset[0]:
    coin_credit_configure+dWriteText+41
    5800
    .byte[3] 00, 30, 60
    ; Amount to add to string IDs to get the localized version for each language
    string_table_localized[0]:
    WriteText+45WriteText+48
    5804
    .ptr[72] str_APPUYER_SUR_START[0], str_CARBURANT_DIMINUE[0], str_PLUS_DE_CARBURANT[0], str_PERDUES[0], str_INTRODUIRE_LES_PIECES[0], str_PAR_PIECE[0], str_RESERVOIR_AUXILIAIRE_DE_CARBURANT_DETRUIT[0], str_FELICITATIONS[0], str_VOUS_AVEZ_ATTERRI_DIFFICILEMENT[0], str_SPLENDIDE_ATTERRISSAGE[0], str_LE_EAGLE_A_ATTERRI[0], str_LE_COLUMBIA_A_ATTERRI[0], str_VOUS_AVEZ_ATTERRI[0], str_LE_SUPPORT_DE_SAUVETAGE_EST_PARTI[0], str_VOTRE_VOYAGE_EST_SANS_RETOUR[0], str_PERDU_SANS_ESPOIR[0], str_SYSTEME_DE_COMMUNICATION_DETRUIT[0], str_SYSTEME_DE_COMMUNICATION_DETRUIT[25], str_VOUS_AVEZ_CREE_UN_CRATERE_DE_DEUX_KILOMETRES[0], str_VOUS_AVEZ_UN_CRASH[0], str_IL_NEY_A_PAS_DE_SURVIVANTS[0], None, str_CHOIX_DU_JEU[0], str__UNITES_DE_CARBURANT_[0], str_PULSAR_START[0], str_POCO_COMBUSTIBLE[0], str_SIN_COMBUSTIBLE[0], str_PERDIDAS[0], str_INSERTE_FICHAS[0], str_POR_FICHA[0], str_TANQUES_AUXILIARES_DE_COMBUSTIBLE_DESTRUIDOS[0], str_FELICITACIONES[0], str_USTED_ALUNIZO_VIOLENTAMENTE[0], str_FUE_UN_GRAN_ALUNIZAJE[0], str_EL_AGUILA_HA_ALUNIZADO[0], str_EL_COLUMBIA_HA_ALUNIZADO[0], str_USTED_HA_ALUNIZADO[0], str_EQUIPO_DE_SUPERVIVENCIA_DESTRUIDO[0], str_SU_VIAJE_ES_DE_IDA_SOLAMENTE[0], str_LAMENTABLEMENTE_USTED_NO_PUEDE_VOLVER[0], str_SISTEMA_DE_COMUNICACION_DESTRUIDO[0], str_SISTEMA_DE_COMUNICACION_DESTRUIDO[24], str_USTED_CREO_UN_CRATER_DE_2_KILOMETROS[0], str_DEMOLER[0], str_NO_HUBO_SOBREVIVIENTES[0], str__PUNTOS[0], str_ELEGIR_JUEGO[0], str__UNIDADES_DE_COMBUSTIBLE_[0], str_STARTKNOEPFE_DRUECKEN[0], str_TREIBSTOFF_GEHT_AUS[0], str_TREIBSTOFFTANKS_LEER[0], str_TEILVERLUST[0], str_GELD_AUSWERFEN[0], str_ZUKAUF_PRO_MUENZE[0], str_ZUSATZTREIBSTOFFTANKS_ZERSTOERT[0], str_GRATULATION[0], str_SIE_SIND_HART_GELANDET[0], str_DIES_WAR_EINE_GROSSARTIGE_LANDUNG[0], str_EAGLE_IST_GELANDET[0], str_COLUMBIA_IST_GELANDET[0], str_SIE_SIND_GELANDET[0], str_LEBENSRETTUNGSSYSTEME_SIND_AUSGEFALLEN[0], str_REISE_OHNE_RUECKKEHR[0], str_KEIN_RUECKSTART_ZUR_ERDE_MOEGLICH[0], str_KOMMUNIKATIONSSYSTEM_ZERSTOERT[0], str_KOMMUNIKATIONSSYSTEM_ZERSTOERT[21], str_SIE_HABEN_EINEN_2_KILOMETER_KRATER_AUFGERISSEN[0], str_50_000_000_MARK_SIND_IN_DIE_LUFT_GEJAGT[0], str_KEINE_UEBERLEBENDEN[0], str__PUNKTE[0], str_SPIELWAHL[0], str__TREIBSTOFF_[0]
    ; Localized strings

    str_APPUYER_SUR_START[0]:
    5804
    5894
    .byte[17] 16, 34, 34, 3e, 46, 1e, 38, 00, 3a, 3e, 38, 00, 3a, 3c, 16, 38, bc
    ; 0 "APPUYER SUR START"
    str_CARBURANT_DIMINUE[0]:
    5804
    58c6
    .byte[17] 1a, 16, 38, 18, 3e, 38, 16, 30, 3c, 00, 1c, 26, 2e, 26, 30, 3e, 9e
    ; 1 "CARBURANT DIMINUE"
    str_PLUS_DE_CARBURANT[0]:
    5804
    58fa
    .byte[17] 34, 2c, 3e, 3a, 00, 1c, 1e, 00, 1a, 16, 38, 18, 3e, 38, 16, 30, bc
    ; 2 "PLUS DE CARBURANT"
    str_PERDUES[0]:
    5804
    592e
    .byte[7] 34, 1e, 38, 1c, 3e, 1e, ba
    ; 3 "PERDUES"
    str_INTRODUIRE_LES_PIECES[0]:
    5804
    5948
    .byte[21] 26, 30, 3c, 38, 32, 1c, 3e, 26, 38, 1e, 00, 2c, 1e, 3a, 00, 34, 26, 1e, 1a, 1e, ba
    ; 4 "INTRODUIRE LES PIECES"
    str_PAR_PIECE[0]:
    5804
    5979
    .byte[9] 34, 16, 38, 00, 34, 26, 1e, 1a, 9e
    ; 5 "PAR PIECE"
    str_RESERVOIR_AUXILIAIRE_DE_CARBURANT_DETRUIT[0]:
    5804
    599c
    .byte[41] 38, 1e, 3a, 1e, 38, 40, 32, 26, 38, 00, 16, 3e, 44, 26, 2c, 26, 16, 26, 38, 1e, 00, 1c, 1e, 00, 1a, 16, 38, 18, 3e, 38, 16, 30, 3c, 00, 1c, 1e, 3c, 38, 3e, 26, bc
    ; 6 "RESERVOIR AUXILIAIRE DE CARBURANT DETRUIT"
    str_FELICITATIONS[0]:
    5804
    5a10
    .byte[13] 20, 1e, 2c, 26, 1a, 26, 3c, 16, 3c, 26, 32, 30, ba
    ; 7 "FELICITATIONS"
    str_VOUS_AVEZ_ATTERRI_DIFFICILEMENT[0]:
    5804
    5a36
    .byte[31] 40, 32, 3e, 3a, 00, 16, 40, 1e, 48, 00, 16, 3c, 3c, 1e, 38, 38, 26, 00, 1c, 26, 20, 20, 26, 1a, 26, 2c, 1e, 2e, 1e, 30, bc
    ; 8 "VOUS AVEZ ATTERRI DIFFICILEMENT"
    str_SPLENDIDE_ATTERRISSAGE[0]:
    5804
    5a86
    .byte[22] 3a, 34, 2c, 1e, 30, 1c, 26, 1c, 1e, 00, 16, 3c, 3c, 1e, 38, 38, 26, 3a, 3a, 16, 22, 9e
    ; 9 "SPLENDIDE ATTERRISSAGE"
    str_LE_EAGLE_A_ATTERRI[0]:
    5804
    5ad2
    .byte[18] 2c, 1e, 00, 1e, 16, 22, 2c, 1e, 00, 16, 00, 16, 3c, 3c, 1e, 38, 38, a6
    ; 10 "LE EAGLE A ATTERRI"
    str_LE_COLUMBIA_A_ATTERRI[0]:
    5804
    5b0c
    .byte[21] 2c, 1e, 00, 1a, 32, 2c, 3e, 2e, 18, 26, 16, 00, 16, 00, 16, 3c, 3c, 1e, 38, 38, a6
    ; 11 "LE COLUMBIA A ATTERRI"
    str_VOUS_AVEZ_ATTERRI[0]:
    5804
    5b4e
    .byte[17] 40, 32, 3e, 3a, 00, 16, 40, 1e, 48, 00, 16, 3c, 3c, 1e, 38, 38, a6
    ; 12 "VOUS AVEZ ATTERRI"
    str_LE_SUPPORT_DE_SAUVETAGE_EST_PARTI[0]:
    5804
    5b82
    .byte[33] 2c, 1e, 00, 3a, 3e, 34, 34, 32, 38, 3c, 00, 1c, 1e, 00, 3a, 16, 3e, 40, 1e, 3c, 16, 22, 1e, 00, 1e, 3a, 3c, 00, 34, 16, 38, 3c, a6
    ; 13 "LE SUPPORT DE SAUVETAGE EST PARTI"
    str_VOTRE_VOYAGE_EST_SANS_RETOUR[0]:
    5804
    5bea
    .byte[28] 40, 32, 3c, 38, 1e, 00, 40, 32, 46, 16, 22, 1e, 00, 1e, 3a, 3c, 00, 3a, 16, 30, 3a, 00, 38, 1e, 3c, 32, 3e, b8
    ; 14 "VOTRE VOYAGE EST SANS RETOUR"
    str_PERDU_SANS_ESPOIR[0]:
    5804
    5c36
    .byte[17] 34, 1e, 38, 1c, 3e, 00, 3a, 16, 30, 3a, 00, 1e, 3a, 34, 32, 26, b8
    ; 15 "PERDU SANS ESPOIR"
    str_SYSTEME_DE_COMMUNICATION_DETRUIT[0]:
    5804
    5c8d
    .byte[32] 3a, 46, 3a, 3c, 1e, 2e, 1e, 00, 1c, 1e, 00, 1a, 32, 2e, 2e, 3e, 30, 26, 1a, 16, 3c, 26, 32, 30, 00, 1c, 1e, 3c, 38, 3e, 26, bc
    ; 16 "SYSTEME DE COMMUNICATION DETRUIT"
    str_VOUS_AVEZ_CREE_UN_CRATERE_DE_DEUX_KILOMETRES[0]:
    5804
    5cec
    .byte[44] 40, 32, 3e, 3a, 00, 16, 40, 1e, 48, 00, 1a, 38, 1e, 1e, 00, 3e, 30, 00, 1a, 38, 16, 3c, 1e, 38, 1e, 00, 1c, 1e, 00, 1c, 1e, 3e, 44, 00, 2a, 26, 2c, 32, 2e, 1e, 3c, 38, 1e, ba
    ; 18 "VOUS AVEZ CREE UN CRATERE DE DEUX KILOMETRES"
    str_VOUS_AVEZ_UN_CRASH[0]:
    5804
    5d6a
    .byte[18] 40, 32, 3e, 3a, 00, 16, 40, 1e, 48, 00, 3e, 30, 00, 1a, 38, 16, 3a, a4
    ; 19 "VOUS AVEZ UN CRASH"
    str_IL_NEY_A_PAS_DE_SURVIVANTS[0]:
    5804
    5daa
    .byte[26] 26, 2c, 00, 30, 1e, 46, 00, 16, 00, 34, 16, 3a, 00, 1c, 1e, 00, 3a, 3e, 38, 40, 26, 40, 16, 30, 3c, ba
    ; 20 "IL NEY A PAS DE SURVIVANTS"
    str_CHOIX_DU_JEU[0]:
    5804
    5e02
    .byte[12] 1a, 24, 32, 26, 44, 00, 1c, 3e, 00, 28, 1e, be
    ; 22 "CHOIX DU JEU"
    str__UNITES_DE_CARBURANT_[0]:
    5804
    5e23
    .byte[21] 00, 3e, 30, 26, 3c, 1e, 3a, 00, 1c, 1e, 00, 1a, 16, 38, 18, 3e, 38, 16, 30, 3c, 80
    ; 23 " UNITES DE CARBURANT "
    str_PULSAR_START[0]:
    5804
    58a5
    .byte[12] 34, 3e, 2c, 3a, 16, 38, 00, 3a, 3c, 16, 38, bc
    ; 24 "PULSAR START"
    str_POCO_COMBUSTIBLE[0]:
    5804
    58d7
    .byte[16] 34, 32, 1a, 32, 00, 1a, 32, 2e, 18, 3e, 3a, 3c, 26, 18, 2c, 9e
    ; 25 "POCO COMBUSTIBLE"
    str_SIN_COMBUSTIBLE[0]:
    5804
    590b
    .byte[15] 3a, 26, 30, 00, 1a, 32, 2e, 18, 3e, 3a, 3c, 26, 18, 2c, 9e
    ; 26 "SIN COMBUSTIBLE"
    str_PERDIDAS[0]:
    5804
    5935
    .byte[8] 34, 1e, 38, 1c, 26, 1c, 16, ba
    ; 27 "PERDIDAS"
    str_INSERTE_FICHAS[0]:
    5804
    595d
    .byte[14] 26, 30, 3a, 1e, 38, 3c, 1e, 00, 20, 26, 1a, 24, 16, ba
    ; 28 "INSERTE FICHAS"
    str_POR_FICHA[0]:
    5804
    5982
    .byte[9] 34, 32, 38, 00, 20, 26, 1a, 24, 96
    ; 29 "POR FICHA"
    str_TANQUES_AUXILIARES_DE_COMBUSTIBLE_DESTRUIDOS[0]:
    5804
    59c5
    .byte[44] 3c, 16, 30, 36, 3e, 1e, 3a, 00, 16, 3e, 44, 26, 2c, 26, 16, 38, 1e, 3a, 00, 1c, 1e, 00, 1a, 32, 2e, 18, 3e, 3a, 3c, 26, 18, 2c, 1e, 00, 1c, 1e, 3a, 3c, 38, 3e, 26, 1c, 32, ba
    ; 30 "TANQUES AUXILIARES DE COMBUSTIBLE DESTRUIDOS"
    str_FELICITACIONES[0]:
    5804
    5a1d
    .byte[14] 20, 1e, 2c, 26, 1a, 26, 3c, 16, 1a, 26, 32, 30, 1e, ba
    ; 31 "FELICITACIONES"
    str_USTED_ALUNIZO_VIOLENTAMENTE[0]:
    5804
    5a55
    .byte[27] 3e, 3a, 3c, 1e, 1c, 00, 16, 2c, 3e, 30, 26, 48, 32, 00, 40, 26, 32, 2c, 1e, 30, 3c, 16, 2e, 1e, 30, 3c, 9e
    ; 32 "USTED ALUNIZO VIOLENTAMENTE"
    str_FUE_UN_GRAN_ALUNIZAJE[0]:
    5804
    5a9c
    .byte[21] 20, 3e, 1e, 00, 3e, 30, 00, 22, 38, 16, 30, 00, 16, 2c, 3e, 30, 26, 48, 16, 28, 9e
    ; 33 "FUE UN GRAN ALUNIZAJE"
    str_EL_AGUILA_HA_ALUNIZADO[0]:
    5804
    5ae4
    .byte[22] 1e, 2c, 00, 16, 22, 3e, 26, 2c, 16, 00, 24, 16, 00, 16, 2c, 3e, 30, 26, 48, 16, 1c, b2
    ; 34 "EL AGUILA HA ALUNIZADO"
    str_EL_COLUMBIA_HA_ALUNIZADO[0]:
    5804
    5b21
    .byte[24] 1e, 2c, 00, 1a, 32, 2c, 3e, 2e, 18, 26, 16, 00, 24, 16, 00, 16, 2c, 3e, 30, 26, 48, 16, 1c, b2
    ; 35 "EL COLUMBIA HA ALUNIZADO"
    str_USTED_HA_ALUNIZADO[0]:
    5804
    5b5f
    .byte[18] 3e, 3a, 3c, 1e, 1c, 00, 24, 16, 00, 16, 2c, 3e, 30, 26, 48, 16, 1c, b2
    ; 36 "USTED HA ALUNIZADO"
    str_EQUIPO_DE_SUPERVIVENCIA_DESTRUIDO[0]:
    5804
    5ba3
    .byte[33] 1e, 36, 3e, 26, 34, 32, 00, 1c, 1e, 00, 3a, 3e, 34, 1e, 38, 40, 26, 40, 1e, 30, 1a, 26, 16, 00, 1c, 1e, 3a, 3c, 38, 3e, 26, 1c, b2
    ; 37 "EQUIPO DE SUPERVIVENCIA DESTRUIDO"
    str_SU_VIAJE_ES_DE_IDA_SOLAMENTE[0]:
    5804
    5c06
    .byte[28] 3a, 3e, 00, 40, 26, 16, 28, 1e, 00, 1e, 3a, 00, 1c, 1e, 00, 26, 1c, 16, 00, 3a, 32, 2c, 16, 2e, 1e, 30, 3c, 9e
    ; 38 "SU VIAJE ES DE IDA SOLAMENTE"
    str_LAMENTABLEMENTE_USTED_NO_PUEDE_VOLVER[0]:
    5804
    5c47
    .byte[37] 2c, 16, 2e, 1e, 30, 3c, 16, 18, 2c, 1e, 2e, 1e, 30, 3c, 1e, 00, 3e, 3a, 3c, 1e, 1c, 00, 30, 32, 00, 34, 3e, 1e, 1c, 1e, 00, 40, 32, 2c, 40, 1e, b8
    ; 39 "LAMENTABLEMENTE USTED NO PUEDE VOLVER"
    str_SISTEMA_DE_COMUNICACION_DESTRUIDO[0]:
    5804
    5cad
    .byte[33] 3a, 26, 3a, 3c, 1e, 2e, 16, 00, 1c, 1e, 00, 1a, 32, 2e, 3e, 30, 26, 1a, 16, 1a, 26, 32, 30, 00, 1c, 1e, 3a, 3c, 38, 3e, 26, 1c, b2
    ; 40 "SISTEMA DE COMUNICACION DESTRUIDO"
    str_USTED_CREO_UN_CRATER_DE_2_KILOMETROS[0]:
    5804
    5d18
    .byte[36] 3e, 3a, 3c, 1e, 1c, 00, 1a, 38, 1e, 32, 00, 3e, 30, 00, 1a, 38, 16, 3c, 1e, 38, 00, 1c, 1e, 00, 06, 00, 2a, 26, 2c, 32, 2e, 1e, 3c, 38, 32, ba
    ; 42 "USTED CREO UN CRATER DE 2 KILOMETROS"
    str_DEMOLER[0]:
    5804
    5d7c
    .byte[7] 1c, 1e, 2e, 32, 2c, 1e, b8
    ; 43 "DEMOLER"
    str_NO_HUBO_SOBREVIVIENTES[0]:
    5804
    5dc4
    .byte[22] 30, 32, 00, 24, 3e, 18, 32, 00, 3a, 32, 18, 38, 1e, 40, 26, 40, 26, 1e, 30, 3c, 1e, ba
    ; 44 "NO HUBO SOBREVIVIENTES"
    str__PUNTOS[0]:
    5804
    5df4
    .byte[7] 00, 34, 3e, 30, 3c, 32, ba
    ; 45 " PUNTOS"
    str_ELEGIR_JUEGO[0]:
    5804
    5e0e
    .byte[12] 1e, 2c, 1e, 22, 26, 38, 00, 28, 3e, 1e, 22, b2
    ; 46 "ELEGIR JUEGO"
    str__UNIDADES_DE_COMBUSTIBLE_[0]:
    5804
    5e38
    .byte[25] 00, 3e, 30, 26, 1c, 16, 1c, 1e, 3a, 00, 1c, 1e, 00, 1a, 32, 2e, 18, 3e, 3a, 3c, 26, 18, 2c, 1e, 80
    ; 47 " UNIDADES DE COMBUSTIBLE "
    str_STARTKNOEPFE_DRUECKEN[0]:
    5804
    58b1
    .byte[21] 3a, 3c, 16, 38, 3c, 2a, 30, 32, 1e, 34, 20, 1e, 00, 1c, 38, 3e, 1e, 1a, 2a, 1e, b0
    ; 48 "STARTKNOEPFE DRUECKEN"
    str_TREIBSTOFF_GEHT_AUS[0]:
    5804
    58e7
    .byte[19] 3c, 38, 1e, 26, 18, 3a, 3c, 32, 20, 20, 00, 22, 1e, 24, 3c, 00, 16, 3e, ba
    ; 49 "TREIBSTOFF GEHT AUS"
    str_TREIBSTOFFTANKS_LEER[0]:
    5804
    591a
    .byte[20] 3c, 38, 1e, 26, 18, 3a, 3c, 32, 20, 20, 3c, 16, 30, 2a, 3a, 00, 2c, 1e, 1e, b8
    ; 50 "TREIBSTOFFTANKS LEER"
    str_TEILVERLUST[0]:
    5804
    593d
    .byte[11] 3c, 1e, 26, 2c, 40, 1e, 38, 2c, 3e, 3a, bc
    ; 51 "TEILVERLUST"
    str_GELD_AUSWERFEN[0]:
    5804
    596b
    .byte[14] 22, 1e, 2c, 1c, 00, 16, 3e, 3a, 42, 1e, 38, 20, 1e, b0
    ; 52 "GELD AUSWERFEN"
    str_ZUKAUF_PRO_MUENZE[0]:
    5804
    598b
    .byte[17] 48, 3e, 2a, 16, 3e, 20, 00, 34, 38, 32, 00, 2e, 3e, 1e, 30, 48, 9e
    ; 53 "ZUKAUF PRO MUENZE"
    str_ZUSATZTREIBSTOFFTANKS_ZERSTOERT[0]:
    5804
    59f1
    .byte[31] 48, 3e, 3a, 16, 3c, 48, 3c, 38, 1e, 26, 18, 3a, 3c, 32, 20, 20, 3c, 16, 30, 2a, 3a, 00, 48, 1e, 38, 3a, 3c, 32, 1e, 38, bc
    ; 54 "ZUSATZTREIBSTOFFTANKS ZERSTOERT"
    str_GRATULATION[0]:
    5804
    5a2b
    .byte[11] 22, 38, 16, 3c, 3e, 2c, 16, 3c, 26, 32, b0
    ; 55 "GRATULATION"
    str_SIE_SIND_HART_GELANDET[0]:
    5804
    5a70
    .byte[22] 3a, 26, 1e, 00, 3a, 26, 30, 1c, 00, 24, 16, 38, 3c, 00, 22, 1e, 2c, 16, 30, 1c, 1e, bc
    ; 56 "SIE SIND HART GELANDET"
    str_DIES_WAR_EINE_GROSSARTIGE_LANDUNG[0]:
    5804
    5ab1
    .byte[33] 1c, 26, 1e, 3a, 00, 42, 16, 38, 00, 1e, 26, 30, 1e, 00, 22, 38, 32, 3a, 3a, 16, 38, 3c, 26, 22, 1e, 00, 2c, 16, 30, 1c, 3e, 30, a2
    ; 57 "DIES WAR EINE GROSSARTIGE LANDUNG"
    str_EAGLE_IST_GELANDET[0]:
    5804
    5afa
    .byte[18] 1e, 16, 22, 2c, 1e, 00, 26, 3a, 3c, 00, 22, 1e, 2c, 16, 30, 1c, 1e, bc
    ; 58 "EAGLE IST GELANDET"
    str_COLUMBIA_IST_GELANDET[0]:
    5804
    5b39
    .byte[21] 1a, 32, 2c, 3e, 2e, 18, 26, 16, 00, 26, 3a, 3c, 00, 22, 1e, 2c, 16, 30, 1c, 1e, bc
    ; 59 "COLUMBIA IST GELANDET"
    str_SIE_SIND_GELANDET[0]:
    5804
    5b71
    .byte[17] 3a, 26, 1e, 00, 3a, 26, 30, 1c, 00, 22, 1e, 2c, 16, 30, 1c, 1e, bc
    ; 60 "SIE SIND GELANDET"
    str_LEBENSRETTUNGSSYSTEME_SIND_AUSGEFALLEN[0]:
    5804
    5bc4
    .byte[38] 2c, 1e, 18, 1e, 30, 3a, 38, 1e, 3c, 3c, 3e, 30, 22, 3a, 3a, 46, 3a, 3c, 1e, 2e, 1e, 00, 3a, 26, 30, 1c, 00, 16, 3e, 3a, 22, 1e, 20, 16, 2c, 2c, 1e, b0
    ; 61 "LEBENSRETTUNGSSYSTEME SIND AUSGEFALLEN"
    str_REISE_OHNE_RUECKKEHR[0]:
    5804
    5c22
    .byte[20] 38, 1e, 26, 3a, 1e, 00, 32, 24, 30, 1e, 00, 38, 3e, 1e, 1a, 2a, 2a, 1e, 24, b8
    ; 62 "REISE OHNE RUECKKEHR"
    str_KEIN_RUECKSTART_ZUR_ERDE_MOEGLICH[0]:
    5804
    5c6c
    .byte[33] 2a, 1e, 26, 30, 00, 38, 3e, 1e, 1a, 2a, 3a, 3c, 16, 38, 3c, 00, 48, 3e, 38, 00, 1e, 38, 1c, 1e, 00, 2e, 32, 1e, 22, 2c, 26, 1a, a4
    ; 63 "KEIN RUECKSTART ZUR ERDE MOEGLICH"
    str_KOMMUNIKATIONSSYSTEM_ZERSTOERT[0]:
    5804
    5cce
    .byte[30] 2a, 32, 2e, 2e, 3e, 30, 26, 2a, 16, 3c, 26, 32, 30, 3a, 3a, 46, 3a, 3c, 1e, 2e, 00, 48, 1e, 38, 3a, 3c, 32, 1e, 38, bc
    ; 64 "KOMMUNIKATIONSSYSTEM ZERSTOERT"
    str_SIE_HABEN_EINEN_2_KILOMETER_KRATER_AUFGERISSEN[0]:
    5804
    5d3c
    .byte[46] 3a, 26, 1e, 00, 24, 16, 18, 1e, 30, 00, 1e, 26, 30, 1e, 30, 00, 06, 00, 2a, 26, 2c, 32, 2e, 1e, 3c, 1e, 38, 00, 2a, 38, 16, 3c, 1e, 38, 00, 16, 3e, 20, 22, 1e, 38, 26, 3a, 3a, 1e, b0
    ; 66 "SIE HABEN EINEN 2 KILOMETER KRATER AUFGERISSEN"
    str_50_000_000_MARK_SIND_IN_DIE_LUFT_GEJAGT[0]:
    5804
    5d83
    .byte[39] 0c, 02, 00, 02, 02, 02, 00, 02, 02, 02, 00, 2e, 16, 38, 2a, 00, 3a, 26, 30, 1c, 00, 26, 30, 00, 1c, 26, 1e, 00, 2c, 3e, 20, 3c, 00, 22, 1e, 28, 16, 22, bc
    ; 67 "50 000 000 MARK SIND IN DIE LUFT GEJAGT"
    str_KEINE_UEBERLEBENDEN[0]:
    5804
    5dda
    .byte[19] 2a, 1e, 26, 30, 1e, 00, 3e, 1e, 18, 1e, 38, 2c, 1e, 18, 1e, 30, 1c, 1e, b0
    ; 68 "KEINE UEBERLEBENDEN"
    str__PUNKTE[0]:
    5804
    5dfb
    .byte[7] 00, 34, 3e, 30, 2a, 3c, 9e
    ; 69 " PUNKTE"
    str_SPIELWAHL[0]:
    5804
    5e1a
    .byte[9] 3a, 34, 26, 1e, 2c, 42, 16, 24, ac
    ; 70 "SPIELWAHL"
    str__TREIBSTOFF_[0]:
    5804
    5e51
    .byte[12] 00, 3c, 38, 1e, 26, 18, 3a, 3c, 32, 20, 20, 80
    ; 71 " TREIBSTOFF "

    Zero Page data

    On the 6502 global variables are often stored on the "zero page" since it is possible to reference the bottom 256 bytes of memory with shorter instruction sequences. It is initialized to zero or a static value in the RESET code.

    ship_angle_modulo:
    ResetGameState+5game_state_reinit_maybe+4edraw_ship_prep_maybe+16ship_compute_accel_xy+0ship_compute_accel_xy+15ship_reset_saved_angle+0abort_procedure_update+fabort_procedure_update+1babort_procedure_update+24abort_procedure_update+33ship_command_yaw+1fvec_letter_something+8S711c+f6ship_command_yaw_easy+13ship_command_yaw_easy+22
    0002
    .byte
    ; Ship angle mod 90 degrees, from 0 - 31.
    mult_y:
    mult16_repeat+0mult16_repeat+15
    0042
    .byte
    ; Argument 1 to multiply
    mult_a:
    mult16+0mult16_repeat+2mult16_repeat+7mult16_repeat+11mult16_repeat+20
    0043
    .byte
    ; Argument 2 to multiply
    mult_acc:
    mult16_repeat+b
    0044
    .word
    ; Multiply accumulator
    ship_angle:
    ship_reset+dship_command_yaw+eship_command_yaw+13
    0066
    .word
    ; 16-bit Ship angle
    min16_a:
    GameRunningLoop+17GameRunningLoop+1f2min16+4min16+emin16+14
    007d
    .word
    ; Arg 1 for min16
    min16_b:
    GameRunningLoop+2aGameRunningLoop+1f9min16+cmin16+12min16+1a
    007f
    .word
    ; Arg 2 for min16

    Coin handling

    This is interesting because it has to deal with potential (physical) attacks on the coin slots, which apparently was a problem with some other arcade cabinets. The memory mapped coin detectors are discussed in the button handling section.

    ; The coin drop switches are only valid if they have been through
    ; 16 debounce timers, which is 16 * NMI/8, or about 0.5 seconds
    CoinDropTimers[0]:
    CheckCoinsInserted+6CheckCoinsInserted+1eCheckCoinsInserted+33CheckCoinsInserted+47CheckCoinsInserted+54coin_inserted+15coin_inserted+19
    00b7
    .byte[3]
    ; Debounce timer for each coin slot
    WaitCoinTimer_0:
    CheckCoinsInserted+35CheckCoinsInserted+38CheckCoinsInserted+3cCheckCoinsInserted+56CheckCoinsInserted+5d
    00b3
    .byte
    ; This is actually an array, but the slam timer is in the middle
    SlamTimer:
    CheckCoinsInserted+29CheckCoinsInserted+2bCheckCoinsInserted+2f
    00b4
    .byte
    ; Counter for the vibration / tilt switch to detect cheating
    WaitCoinTimer_2:
    CheckCoinsInserted+29CheckCoinsInserted+2bCheckCoinsInserted+2f
    00b4
    .byte
    ; Coin slot 2 timer
    coin_check_counter:
    CheckCoinsInserted+13CheckCoinsInserted+69CheckCoinsInserted+6b
    00ba
    .byte
    ; Track how often the coin handling routine is called

    ; There are three coin buttons with active high coin detectors.
    ; They are potentially bouncy, so timers are used to try to avoid multiple triggers.
    ; And the player might be slamming the cabinet to try to trigger the sensor,
    ; so also check the vibration sensor.
    ;
    ; Note that only the two coin slots are used (0 and 2)
    ;
    ; Returns a set carry flag if a coin was detected.
    ;
    CheckCoinsInserted:
    NMI_handler+46
    78d3
    a202
    LDX #$02
    ; for each coin slot = 2, 0: (middle one is ignored)
    check_coin:
    793a
    78d5
    bd0124
    ; read coin[i] from external device
    78d8
    0a
    ASL
    ; move bit 7 into the carry flag (0 == no coin, 1 == coin)
    78d9
    b5b7
    ; read the debounce timer for this coin slot
    78db
    291f
    AND #$1f
    ; mask out the bottom bits of the debounce
    78dd
    b037
    ; if carry flag is set (coin) check how long it has been active
    78df
    f010
    ; if the debounce timer is 0, check if the slam switch has been triggered
    78e1
    c91b
    CMP #$1b
    ; if the debounce timer is less than 0x1b
    78e3
    b00a
    ; then goto debounce timer decrement
    78e5
    a8
    TAY
    ; store the debounce timer in Y
    78e6
    a5ba
    ; only decrement the debounce timer every
    78e8
    2907
    AND #$07
    ; eight NMI clock cycles
    78ea
    c907
    CMP #$07
    ; which is 250/8 or about 30 Hz
    78ec
    98
    TYA
    ; restore the debounce timer
    78ed
    9002
    ; skip the decrement until the eight NMI clock
    DecDropTimer:
    78e3
    78ef
    e901
    SBC #$01
    ; decrement the debounce timer
    CheckSlamSw:
    78df78ed791e7925
    78f1
    95b7
    ; store the (possibly updated) debounce timer
    78f3
    ad0020
    LDA IO_in0
    ; read the memory mapped switches at 0x2000
    78f6
    2904
    AND #$04
    ; check bit 2, the slam switch
    78f8
    d004
    ; if it is not set, goto the slam timer check
    78fa
    a9f0
    LDA #$f0
    ; it is set, set the slam timer counter at 0xF0
    78fc
    85b4
    ; and store it
    CheckSlamTimer:
    78f8
    78fe
    a5b4
    ; Read the slam timer
    7900
    f008
    ; If it is zero, check to see how the coin debouncers are doing
    7902
    c6b4
    ; We're still waiting for slam timer to expire, so
    7904
    a900
    LDA #$00
    ; zero out the debounce counters
    7906
    95b7
    ; for this coin slot
    7908
    95b3
    ; so that no coins will be accepted
    CheckWaitTimer:
    7900
    790a
    18
    CLC
    ; clear carry flag
    790b
    b5b3
    ; is this coin slot in a cool down?
    790d
    f023
    ; yes, goto check the next slot
    790f
    d6b3
    ; decrement the cool down timer
    7911
    d01f
    ; if it is not yet zero, check the next coin slot
    7913
    38
    SEC
    ; set carry (indicating no coins from this slot)
    7914
    b01c
    ; and go check the next coin slot
    CheckDropTimerVal:
    78dd
    7916
    c91b
    CMP #$1b
    ;
    7918
    b009
    ;
    791a
    b5b7
    ;
    791c
    6920
    ADC #$20
    ;
    791e
    90d1
    ;
    7920
    f001
    ;
    7922
    18
    CLC
    ; clear carry (no coins)
    ResetDropTimer:
    79187920
    7923
    a91f
    LDA #$1f
    ;
    7925
    b0ca
    ;
    7927
    95b7
    ; Reset the debounce timer for this slot
    7929
    b5b3
    ; and the check if the cool down timer
    792b
    f001
    ; is zero (which means we haven't been waiting for a coin) so don't set the carry flag
    792d
    38
    SEC
    ; cool down was non-zero, so it is time to give them a credit by setting the carry
    SetWaitTimer:
    792b
    792e
    a978
    LDA #$78
    ; Initialze the cool down timer to 120
    7930
    95b3
    ; which is about 1/2 a second between coins
    CheckNextMech:
    790d79117914
    7932
    9004
    ; if carry is not set, there is no coin in this slot, go to the next one
    7934
    f6b6
    ; carry was set, so increment the number of credits for this slot!
    7936
    e6b2
    ; and also increment the total number of coins inserted
    DoNextCoinMech:
    7932
    7938
    ca
    DEX
    ; double decrement the index
    7939
    ca
    DEX
    ; since the middle one is ignored
    793a
    1099
    ; if it is non-negative, make another loop
    793c
    e6ba
    ; increment our counter for each time the function is called
    793e
    a5ba
    ; Re read it the number of calls
    7940
    4a
    LSR
    ; Shift the bottom bit of the counter into the carry
    7941
    a5b2
    ; Read the number of valid coins
    7943
    b00c
    ; if this is an odd numbered call, goto EndCoinCheck
    7945
    f00a
    ; if there are no valid coins, goto EndCoinCheck
    7947
    c910
    CMP #$10
    ; if the valid coins are more than 10
    7949
    b002
    ; ??
    794b
    69ff
    ADC #$ff
    ; ??
    NextValidCoin_maybe:
    7949
    794d
    69ef
    ADC #$ef
    ; ??
    794f
    85b2
    ; Store this result back in ValidCoins
    EndCoinCheck:
    79437945
    7951
    0a
    ASL
    ; Shift it left (into the carry)
    7952
    60
    RTS
    ; and return the result to the caller

    Game tick

    The mainboard is configured with an external timer that is running at 250 Hz and triggers and NMI that is used to drive the main timing loop. When an NMI is received, the 6502 will push some state on the stack, read the pointer from 0xFFa and jump to that address

    NMI_vector:

    7ffa
    .word 7aa8
    ; NMI vector pointer
    thrust_high:
    RESET+23NMI_handler+aNMI_handler+ethrust_smoothing_maybe+3a
    0083
    .byte
    ; High point for the thrust level (used for smoothing)
    thrust_low:
    NMI_handler+12NMI_handler+16NMI_handler+18thrust_smoothing_maybe+8thrust_smoothing_maybe+16thrust_smoothing_maybe+25thrust_smoothing_maybe+3cthrust_smoothing_maybe+3e
    0084
    .byte
    ; Low point for the thrust level (used for smoothing)
    dvg_timer:
    NMI_handler+30NMI_handler+a6dvg_wait_done+0
    0073
    .byte
    ; Waits for acks from the vector generator
    nmi_counter:
    NMI_handler+2eNMI_handler+90ResetGameState+18game_state_reinit_maybe+0game_state_reinit_maybe+dlanded_choose_random+22
    0074
    .byte
    ; Track how many NMI have been received
    nmi_counter_250:
    NMI_handler+5cNMI_handler+62
    0087
    .byte
    ; Count every 250 NMI's to create a 1 Hz counter
    time_in_seconds:
    NMI_handler+64GameLoop+1affuel_drain+28
    008d
    .byte
    ; Increments once per second, triggered by nmi_counter_250 going to zero
    score_bcd_maybe[0]:
    NMI_handler+6eNMI_handler+70fuel_lost_to_crash+1fuel_lost_to_crash+7fuel_lost_to_crash+c
    009e
    .byte[3]
    ; BCD score value (6 digits, not sure how different from a4)
    time_bcd[0]:
    NMI_handler+78NMI_handler+83NMI_handler+85NMI_handler+89game_state_reinit_maybe+3e
    009c
    .byte[2]
    ; BCD mission timer (4 digits, first byte is seconds, second byte is minutes)
    ; 7------- Exploding?
    ; -6------ Game playing?
    ; --5----- Start screen
    ; ---4---- Attract mode?
    game_state_flags:
    NMI_handler+58NMI_handler+8cGameLoop+6GameLoop+13GameLoop+34GameLoop+4aGameLoop+6bGameLoop+7dGameLoop+83GameLoop+a7GameLoop+107GameLoop+133GameLoop+142GameLoop+149GameLoop+176GameLoop+1fbgame_state_reinit_maybe+45vec_draw_subroutines+10thrust_smoothing_maybe+0thrust_smoothing_maybe+46
    0022
    .byte
    ; Bitmask of the current game mode

    ; NMI Handler invoked at 250 Hz
    ; This is the core game tick that invokes all of the ship updates and reads from the player.
    ; It ensures that the vector drawing system is making progress
    ; and also pets the watchdog to ensure that the system doesn't reset.
    NMI_handler:

    7aa8
    48
    PHA
    ; Push the accumulator onto the stack
    7aa9
    8a
    TXA
    ; Move X to A
    7aaa
    48
    PHA
    ; Push X to the stack
    7aab
    98
    TYA
    ; Move Y to A
    7aac
    48
    PHA
    ; Push Y to the stack
    7aad
    d8
    CLD
    ; Clear decimal mode
    nmi_thrust_process:

    7aae
    ad002c
    ; Read the thrust potentiometer for low pass smoothing
    7ab1
    38
    SEC
    ; Clear carry for SBC
    7ab2
    e583
    ; if IO_thrust > thrust_high
    7ab4
    b004
    ; skip decrement
    7ab6
    c683
    ; decay thrust_high towards the reading
    7ab8
    a900
    LDA #$00
    ; force skip increment
    thrust_higher:
    7ab4
    7aba
    c584
    ; if IO_thrust < thrust_low
    7abc
    9004
    ; skip increment
    7abe
    e684
    ; Increase thrust_low towards the reading
    7ac0
    a584
    ;
    thrust_lower:
    7abc
    7ac2
    aa
    TAX
    ;
    7ac3
    38
    SEC
    ; Clear carry for SBC
    7ac4
    e582
    ;
    7ac6
    900a
    ;
    7ac8
    4a
    LSR
    ;
    7ac9
    4a
    LSR
    ;
    7aca
    f00a
    ;
    L7acc:
    7ad4
    7acc
    8682
    ;
    7ace
    e685
    ;
    7ad0
    d004
    ;
    L7ad2:
    7ac6
    7ad2
    6903
    ADC #$03
    ;
    7ad4
    30f6
    ;
    nmi_check_dvg:
    7aca7ad0
    7ad6
    e674
    ; Track the number of NMI's received
    7ad8
    a573
    ; If the vector generator has been stalled
    7ada
    c903
    CMP #$03
    ; for more than 4 NMI's
    7adc
    b00d
    ; then just wait for the watchdog
    7ade
    8d0034
    ; else pet the watchdog
    7ae1
    a500
    ;
    7ae3
    45c1
    ;
    7ae5
    45c2
    ;
    7ae7
    c985
    CMP #$85
    ;
    7ae9
    f003
    ;
    wait_for_watchdog:
    7adc7aeb
    7aeb
    4ceb7a
    ; Infinite loop, waiting for the watchdog to bark
    nmi_check_coins:
    7ae9
    7aee
    20d378
    ; Has the player inserted any new coins?
    7af1
    9006
    ; Carry clear == no coins, goto
    7af3
    a220
    LDX #$20
    ; set coin counter output lamp
    7af5
    a9ff
    LDA #$ff
    ; keep all the other lamps the same
    7af7
    d004
    ; (always taken)
    nmi_no_coins:
    7af1
    7af9
    a9df
    LDA #$df
    ; unset coin counter output lamp
    7afb
    a200
    LDX #$00
    ; also reset the rest of them
    nmi_update_lights:
    7af7
    7afd
    205f79
    ; update the lamps based on if a coin was received
    7b00
    2422
    ; Is there a game in progress?
    7b02
    502f
    ; If bit 6 is not set (no game playing) do not update the per second timer
    7b04
    c687
    ; Decrement the 250 divider
    7b06
    d02b
    ; if it is non zero, skip ahead
    7b08
    a9fa
    LDA #$fa
    ; Reset the divider with 250 (0xFA)
    7b0a
    8587
    ; and store it in the global
    7b0c
    e68d
    ; Increment our once-per-second counter
    7b0e
    f8
    SED
    ; Enable decimal mode to increment score
    7b0f
    18
    CLC
    ; Clear the carry for the addition
    7b10
    a200
    LDX #$00
    ; for x = 0, 1, 2
    7b12
    a002
    LDY #$02
    ; (although y is used for the counter)
    7b14
    a908
    LDA #$08
    ; add 8 to the score
    nmi_score_bcd_update:
    7b1e
    7b16
    759e
    ; add A to the bcd score[X]
    7b18
    959e
    ; and store it back in bcd store[X]
    7b1a
    a900
    LDA #$00
    ; zero A
    7b1c
    e8
    INX
    ; x++
    7b1d
    88
    DEY
    ; y--
    7b1e
    10f6
    ; if y > 0 do another digit
    nmi_time_bcd_update:

    7b20
    a59c
    ; get the first digit of the BCD timer
    7b22
    18
    CLC
    ; clear the carry
    7b23
    6901
    ADC #$01
    ; add one to the seconds timer
    7b25
    c960
    CMP #$60
    ; if it has not reached 60
    7b27
    9002
    ; then goto no overflow
    7b29
    a900
    LDA #$00
    ; otherwise zero the seconds
    nmi_time_seconds_no_overflow:
    7b27
    7b2b
    859c
    ; and store it back in the LSB
    7b2d
    a59d
    ; load the minutes of the timer
    7b2f
    6900
    ADC #$00
    ; and if the seconds overflowed 60, add it to the minutes
    7b31
    859d
    ; store it back in the MSB
    not_a_second:
    7b027b06
    7b33
    d8
    CLD
    ; exit decimal mode (whew)
    7b34
    a522
    ; read the current game state
    7b36
    d00e
    ; if any bits are set,
    7b38
    a574
    ; game state is zero, so no game right now
    7b3a
    a21f
    LDX #$1f
    ; let's make the lights flash (1F == *all the lamps*)
    7b3c
    4a
    LSR
    ; shift the bottom bit from the nmi counter into carry
    7b3d
    9002
    ; every other NMI turn on all the lamps
    7b3f
    a210
    LDX #$10
    ; and every odd one turn off all the lamps
    L7b41:
    7b3d
    7b41
    a900
    LDA #$00
    ; don't keep any of the old bits
    7b43
    205f79
    ; and toggle the lamps
    L7b46:
    7b36
    7b46
    c68a
    ; divide the NMI counter by 6 for the vector generator timer
    7b48
    d006
    ; if it is not zero, just return
    7b4a
    a906
    LDA #$06
    ; reset the dvg divisor
    7b4c
    858a
    ; store it back in the delay counter
    7b4e
    e673
    ; and increment the vector generator timer
    nmi_rti:
    7b48
    7b50
    68
    PLA
    ; Pop the CPU state from the stack
    7b51
    a8
    TAY
    ; -> Y
    7b52
    68
    PLA
    ; and
    7b53
    aa
    TAX
    ; -> X
    7b54
    68
    PLA
    ; -> A
    7b55
    40
    RTI
    ; Return from interrupt