(back to project page)

Missile Command Revision 2 vs. 3

The revision 2 ROMs have some significant bugs that are exploited during world record score attempts, particularly in "marathon" games. Revision 2 is effectively the "standard" version, even though the less-common revision 3 software is more correct.

Changes between rev 2 and rev 3:

  1. Increase difficulty on waves 17-19+.
  2. Rework bonus city award calculation to fix "810" bug.
  3. Reset wave to 20 when wave 40 is reached.

The easiest way to see the bugs in practice is to set the appropriate wave number or score value using an emulator. This video demonstrates how to jump to wave 255 in MAME, using the debugger.

It's possible to identify a rev 3 ROM by looking for an "eclipse" on the "MISSILE COMMAND" title page. In order to make room for the wave-reset patch, the code that draws the title explosions was rewritten. The original version erased all 20 existing explosions, then drew 20 new ones. Any explosions that overlap would simply blend together. The new version erases and redraws each explosion individually, so if a new explosion in slot #1 overlaps an older explosion in slot #2, then part or all of the new explosion in slot #1 will disappear when slot #2 is erased. It can be hard to spot with the colors cycling, but if you watch carefully you will often see an explosion with a bite taken out of it.

eclipse1 eclipse2

If you see an eclipse, you know you have a revision 3 ROM set. There's also a minor change to the sizes of explosions, but that's even harder to spot.

Increased Difficulty

The number of ICBMs per wave was increased for waves 17-19+. The details can be found on the wave guide page.

This doesn't address a bug. It just makes the game harder.

Bonus City Award Calculation

The code that awards bonus cities doesn't increment the count as you play. Instead, at the end of each wave, it figures out the total number of bonus cities your score represents, and then subtracts the number of bonus cities awarded in the past. For example, if your score is 805,000, and bonus cities are awarded every 10,000 points, it would conclude that you have earned 80 bonus cities.

The code is trying to divide the player's score by the bonus city score, but the 6502 isn't very good at division, so it just subtracts the bonus city score (10,000) from the player's score (805,000) until the value goes negative, incrementing a counter each time. The subtraction is done in in BCD (binary-coded decimal) mode, with the score held in three bytes. In this example the bytes are $80 $50 $00, from which we repeatedly subtract $01 $00 $00.

The problem occurs when the player's score reaches a level where the first subtraction leaves a value of $80 or higher in the high byte. In our example, if the score were 810,000 ($81 $00 $00) and we subtracted 10,000 ($01 $00 $00) we would be left with $800,000 ($80 $00 $00), which is a negative number as far as the 6502 is concerned. The subtraction loop exits immediately, which effectively means that the game has decided that you've earned a grand total of zero bonus cities.

Now for the fun part: the game subtracts the previously-awarded bonus cities from zero. Assuming we finished an earlier wave with a score between 800K and 810K, we should have a total of 80 cities awarded. 0 - 80 = -80, unless you're using 8-bit unsigned math, in which case it's equal to 256 - 80 = 176. So the game awards 176 bonus cities in one shot.

The player's total bonus city count is also stored in an 8-bit value, which rolls over at 256. If the player had lost exactly 6 cities, they would have a total of 80 cities (live + banked bonus), and the addition of 176 more would bring the total to 256... which would roll over to 0, ending the game.

After awarding the huge pile of cities, the previously-awarded bonus city count is set to zero. When the next wave ends, the subtraction loop still ends early, with a result of zero. When the previous count is subtracted from the current count, 0 - 0 = 0, so no bonus cities are awarded. This situation persists until the score rolls over at 1,000,000 points.

Fix

The bug was fixed by rewriting the routine at $6040. The new code mostly fit in the same space as the original, though the code right before it was modified slightly to make a CLD-then-RTS accessible.

Wave Number Reset

There are multiple problems associated with high wave numbers:

The game counts waves from 1, not 0, so wave 1 is $01, wave 255 is $ff, and wave 256 uses the unexpected value $00.

Score Multiplier Bug

The multiplier is computed by taking the current wave number, adding one, and dividing by two. If the result is greater than 6, it's capped at 6. The problem happens on wave 255 and 256, when ((wave+1)/2) evalutes to zero. Zero is less than 6, so it isn't capped, but when it's used as a loop counter it runs for 256 iterations rather than zero. This causes the base score to be set to 25*256 instead of 25*6. (See $5b9e.)

No-Attack Bug

A wave ends after a certain number of ICBMs and smart bombs have been destroyed. The missile and bomb counts are read out of a table, using the wave number as an index. The wave number starts at 1 and is capped at 19, but wave 256 acts like wave 0. The values stored in ROM where the wave 0 values would live happen to be zero, so wave 256 ends after zero missiles and bombs have been destroyed. (See $5b31.)

Difficulty Reset

At wave 257, the wave counter has rolled over completely and is back to 1. The game difficulty resets to match.

Pacing Bug

As mentioned on the main page, the game has a mechanism that blocks the creation of new ICBMs / bombs when the altitude of the highest ICBM is above a certain level. This starts at 200 and drops on each wave, using the formula 202 - 2 * wave_number. The threshold is capped to go no lower than 180.

At wave 102 the result of the calculation becomes negative, which in unsigned 2's complement math makes it very large. This effectively disables the restriction, because the altitude limiter is off the top of the screen. The desired behavior gradually reasserts itself, returning to the wave 1 behavior at wave 129, but breaks again at wave 230.

The bug causes a noticeable change in difficulty at wave 102.

Fix

The fix is simple: when the wave counter reaches 40, roll it back to 20. This works because the difficulty is the same at level 19+, and the color scheme rotates through a 20-wave sequence.

The only trick was finding a place to put the patch. The developers needed 13 bytes, so they rewrote the title explosion animation code and dropped it in there ($6300).


Change Details

For reference, here are the raw diffs for the only file that changed, 035822-02.kl1 vs. 035822-03e.kl1:

4,9c4,9
< 00000030: b9da 0165 d399 da01 85d3 d8a9 ff85 d460  ...e...........`
< 00000040: a5f4 2970 c970 f039 4a4a 4aa8 a900 858d  ..)p.p.9JJJ.....
< 00000050: a6b9 bdd8 0185 98bd da01 8599 f8a5 9838  ...............8
< 00000060: f982 6085 98a5 99f9 8360 8599 3002 e68d  ..`......`..0...
< 00000070: 10eb d8a5 8d38 f5c3 1875 c095 c0a5 8d95  .....8...u......
< 00000080: c360 0001 2001 4001 5001 8001 0002 8000  .`.. .@.P.......
---
> 00000030: b9da 0165 d399 da01 85d3 a9ff 85d4 d860  ...e...........`
> 00000040: a5f4 2970 4a4a 4a4a a8b9 7b60 8599 f0ef  ..)pJJJJ..{`....
> 00000050: a6b9 bdd8 014a 4a4a 4a85 98bd da01 0a0a  .....JJJJ.......
> 00000060: 0a0a 0598 f838 f5c3 38e5 9990 d1f6 c048  .....8..8......H
> 00000070: b5c3 1865 9995 c368 4c68 6010 1214 1518  ...e...hLh`.....
> 00000080: 2008 0001 2001 4001 5001 8001 0002 8000   ... .@.P.......
11c11
< 000000a0: 1012 14d0 e0c0 08a0 6040 2010 0a06 0402  ........`@ .....
---
> 000000a0: 1113 16d0 e0c0 08a0 6040 2010 0a06 0402  ........`@ .....
44,49c44,49
< 000002b0: fdfc fba9 0085 9aa2 13bd 6e01 f014 859c  ..........n.....
< 000002c0: bd39 0185 9bbd c201 85b5 2071 5ea9 009d  .9........ q^...
< 000002d0: 6e01 ca10 e4a9 fc85 eca2 13ad 0a40 2907  n............@).
< 000002e0: 0903 9dc2 0185 9aad 0a40 9d39 0185 9bad  .........@.9....
< 000002f0: 0a40 c9d0 b005 c938 b003 38b0 f29d 6e01  .@.....8..8...n.
< 00000300: 859c a900 85b5 2071 5eca 10cf 6020 4d63  ...... q^...` Mc
---
> 000002b0: fdfc fba2 13a9 0085 9abd 6e01 f006 bdc2  ..........n.....
> 000002c0: 0120 f162 a9fc 85ec ad0a 4029 0709 049d  . .b......@)....
> 000002d0: c201 859a ad0a 409d 3901 ad0a 40c9 d0b0  ......@.9...@...
> 000002e0: f9c9 3890 f59d 6e01 a900 20f1 62ca 10c5  ..8...n... .b...
> 000002f0: 6085 b5bd 6e01 859c bd39 0185 9b4c 715e  `...n....9...Lq^
> 00000300: a5a7 c928 9002 a914 85a7 4c86 6920 4d63  ...(......L.i Mc
86c86
< 00000550: 00a5 9881 0de6 0d88 10e7 605e a980 8509  ..........`^....
---
> 00000550: 00a5 9881 0de6 0d88 10e7 6016 a980 8509  ..........`.....
115c115
< 00000720: ca10 fb20 8669 207c 6820 5e67 2021 69a5  ... .i |h ^g !i.
---
> 00000720: ca10 fb20 0063 207c 6820 5e67 2021 69a5  ... .c |h ^g !i.

Source code for revision 2. Compare to the main disassembly listing.

603a: d8                           cld                       ;rev3: CLD moved down for code sharing
603b: a9 ff                        lda     #$ff
603d: 85 d4                        sta     score_dirty_flag
603f: 60           :Return         rts

                   ; 
                   ; Awards bonus cities based on score.
                   ; 
                   ; Player scores are stored as a 3-byte BCD value: ABC,DEF.  Bonus cities are
                   ; awarded every XY,000 points.
                   ; 
                   ; It's possible to earn multiple bonus cities on one wave.
                   ; 
                   ; On entry:
                   ;   X-reg=player number (0/1)
                   ; 
                   ]bonus_count    .var    $8d    {addr/1}
                   ]score_md       .var    $98    {addr/1}
                   ]score_hi       .var    $99    {addr/1}

6040: a5 f4        CheckBonusCity  lda     r8_irq_inv        ;get R8 switches
6042: 29 70                        and     #%01110000        ;mask to get bonus city bits
6044: c9 70                        cmp     #%01110000        ;are bonus cities disabled?
6046: f0 39                        beq     :Return           ;yes, bail
6048: 4a                           lsr     A                 ;right-shift to get 0-7
6049: 4a                           lsr     A
604a: 4a                           lsr     A
604b: a8                           tay                       ;copy to Y-reg to use as index
604c: a9 00                        lda     #$00
604e: 85 8d                        sta     ]bonus_count      ;init bonus count to zero
                   ; 
6050: a6 b9                        ldx     cur_plyr_num      ;get current player number (0/1)
6052: bd d8 01                     lda     cur_score_md,x    ;get middle byte of score
6055: 85 98                        sta     ]score_md         ;copy to ZP
6057: bd da 01                     lda     cur_score_hi,x    ;get high byte of score
605a: 85 99                        sta     ]score_hi         ;copy to ZP
605c: f8                           sed                       ;enable decimal mode
                   ; Divide the player's score by the bonus city score.
605d: a5 98        :DivLoop        lda     ]score_md         ;get middle byte of score
605f: 38                           sec
6060: f9 82 60                     sbc     bonus_city_bcd,y  ;subtract middle byte of bonus city score
6063: 85 98                        sta     ]score_md         ;save result
6065: a5 99                        lda     ]score_hi         ;repeat for high byte
6067: f9 83 60                     sbc     bonus_city_bcd+1,y
606a: 85 99                        sta     ]score_hi
                   ; Here's the bug: if, after subtracting the bonus city points, the high byte of
                   ; the player's score is >= $80 (i.e. 800,000 points), we branch.  This is
                   ; because, in 2's complement math, $80 is a negative number.
606c: 30 02                        bmi     :DivDone          ;branch if negative
606e: e6 8d                        inc     ]bonus_count      ;add one bonus city
6070: 10 eb        :DivDone        bpl     :DivLoop
                   ; 
6072: d8                           cld                       ;disable decimal mode
6073: a5 8d                        lda     ]bonus_count      ;get the bonus count
6075: 38                           sec
                   ; Here's the effect of the bug: we exited the divide early, so bonus_count is
                   ; zero.  We now subtract the number of bonus cities we previously awarded,
                   ; e.g. at 800K points we awarded the 80th bonus city.  In mod 256 arithmetic 0-
                   ; 80=176, so we give 176 cities this round.  Because we then set the previously-
                   ; awarded value to zero, future score increases award no cities until the score
                   ; rolls over at 1M points.
6076: f5 c3                        sbc     bonus_award_sc,x  ;subtract cities previously awarded to this player
6078: 18                           clc                       ; to get the number of cities to award now
6079: 75 c0                        adc     num_cities,x      ;add the new bonus cities to their collection
607b: 95 c0                        sta     num_cities,x
607d: a5 8d                        lda     ]bonus_count      ;save the updated bonus-city count
607f: 95 c3                        sta     bonus_award_sc,x  ; to the per-player state
6081: 60           :Return         rts

[...]

6090: 0c 0f 12 0c+ icbm_count_tbl  .bulk   $0c,$0f,$12,$0c,$10,$0e,$11,$0a,$0d,$10,$13,$0c,$0e,$10,$12,$0e
                                    +      $10,$12,$14

[...]

62b3: a9 00        DrawTitleExpl   lda     #$00
62b5: 85 9a                        sta     ]end_radius       ;prep to erase (shrink to zero)
62b7: a2 13                        ldx     #19               ;20 explosions
62b9: bd 6e 01     :EraseLoop      lda     expl_ypos,x       ;is there an explosion in this slot?
62bc: f0 14                        beq     :NextE            ;no, branch
62be: 85 9c                        sta     ]yc               ;set Y coord arg
62c0: bd 39 01                     lda     expl_xpos,x       ;get X coord
62c3: 85 9b                        sta     ]xc               ;set arg
62c5: bd c2 01                     lda     expl_radius_idx_arr,x ;get radius
62c8: 85 b5                        sta     ]start_radius     ;set as start radius
62ca: 20 71 5e                     jsr     DrawExplosion     ;call draw routine to erase it
62cd: a9 00                        lda     #$00
62cf: 9d 6e 01                     sta     expl_ypos,x       ;clear explosion entry
62d2: ca           :NextE          dex                       ;move to next entry
62d3: 10 e4                        bpl     :EraseLoop        ;loop until done
                   ; 
62d5: a9 fc                        lda     #%11111100        ;cycle all colors except 0/1 (background)
62d7: 85 ec                        sta     color_cyc_mask
62d9: a2 13                        ldx     #19               ;20 explosions
62db: ad 0a 40     :DrawLoop       lda     POKEY_RANDOM      ;get random number
62de: 29 07                        and     #%00000111        ;reduce to 0-7
62e0: 09 03                        ora     #%00000011        ;make it 3 or 7
62e2: 9d c2 01                     sta     expl_radius_idx_arr,x ;set radius in table
62e5: 85 9a                        sta     ]end_radius       ;set argument
62e7: ad 0a 40                     lda     POKEY_RANDOM      ;get random number
62ea: 9d 39 01                     sta     expl_xpos,x       ;set as X coordinate in table
62ed: 85 9b                        sta     ]xc               ; and in argument
62ef: ad 0a 40     :Retry          lda     POKEY_RANDOM      ;get random number
62f2: c9 d0                        cmp     #208
62f4: b0 05                        bcs     :BcsRetry         ;too high, try again
62f6: c9 38                        cmp     #56
62f8: b0 03                        bcs     :DoDraw           ;high enough, branch
62fa: 38                           sec
62fb: b0 f2        :BcsRetry       bcs     :Retry            ;(always)

62fd: 9d 6e 01     :DoDraw         sta     expl_ypos,x       ;save Y coord in table
6300: 85 9c                        sta     ]yc               ; and in argument
6302: a9 00                        lda     #$00
6304: 85 b5                        sta     ]start_radius     ;set start radius to zero
6306: 20 71 5e                     jsr     DrawExplosion     ;draw the explosion
6309: ca                           dex                       ;have we drawn all 20?
630a: 10 cf                        bpl     :DrawLoop         ;no, branch
630c: 60                           rts

[...]

655b: 5e                           .dd1    $5e               ;checksum byte

[...]

6723: 20 86 69                     jsr     ClearScreen       ;clear entire screen

Copyright 2021 by Andy McFadden