General ------- The Atari 8-bit port of Defender is (c) 1982, Atari Corporation. Author is Steve A. Baker. Interviews with him: http://www.ataricompendium.com/archives/interviews/steve_baker/interview_steve_baker.html http://www.mobygames.com/developer/sheet/view/developerId,153781/ Defender is a 16K cartridge, no bankswitching, maps to $8000-$BFFF. The cartridge-present byte for Cartridge B is non-zero, so the OS doesn't try to init or start the lower half of the cart separately. The game only uses RAM up to $3FFF [1], so it'll run on a 16K Atari (I first played it on a 400). There's almost 1K of filler bytes at the end of the cart (just before the cartridge start address). The code isn't really optimized heavily for space [2] (e.g. lots of jsr followed by rts, or jmp to an rts instead of saving 2 bytes by coding another rts), so prospective hack authors could make more room if needed. The author likes to use "sec : ror someflag" to set booleans, and "lsr someflag" to clear them, then later checks them with either "lda someflag" or "bit someflag" followed by bmi or bpl. Nothing wrong with that, it's just a fairly uncommon technique, so I mention it here. The game doesn't take advantage of the Atari hardware as much as it might. It doesn't use player/missile graphics, or hardware scrolling. This is likely because Baker first wrote the game for the Apple II (sadly, never released [3]), then ported it to (or rewrote it for) the Atari. Also, it was his first game on the Atari. [1] Actually, the code that clears the graphics screen writes a few bytes past $3fff, but nothing ever tries to read them, so no harm done. [2] Don't take that as a criticism. It just means the code fits comfortably in a 16K ROM, so there was no need to spend time shrinking it. [3] There is an Atarisoft Defender release for the Apple II, but it's nothing to do with Steve Baker's Defender. It looks and plays completely different, there's no way its codebase is related to Atari 8-bit Defender. Memory Map ---------- $80-$ff: various game-state and other variables, temporaries, pointers ... $1b00-$1eff: precalculated data tables $1f00-$1fff: self-modifying code copied to RAM $2000-$20c6: display list (only one for the whole game, never changes) $2218-$3fc7: video memory (7600 bytes) Graphics -------- The entire game uses a Mode E (GR.15) display. Display list: 2000: 3x 8 BLANK 2003: LMS 2218 MODE E 2006: DLI MODE E 2007: 87x MODE E 205E: LMS 3000 MODE E 2061: 100x MODE E 20C5: DLI MODE E 20C6: JVB 2000 ...total of 190 scanlines (real GR.15 is 192), contiguous screen memory running from $2218 to $3fc7. There's 2 DLIs, one on the 2nd visible scanline, the other at the bottom. Both use dli_handler, which checks VCOUNT to decide what to do. 2nd LMS is needed to cross a 4K boundary ($2xxx -> $3xxx). Game doesn't use player/missile graphics at all. All graphics are "soft sprites" being blitted into video memory. Elements of the game display: I've named these all and tried to stick strictly to the naming system. There are 4 "screens": the game-select screen (DEFENDER (c) 1982 ATARI), the gameplay screen, the "prompt" screen (PLAYER ONE or PLAYER TWO, in a 2-player game; not seen in 1-player), and the game-over screen. Soft sprites are only rendered on the gameplay screen, and even there only in the playfield (see below). The top of the gameplay screen has areas on the left and right for the players' scores, lives, smartbombs displays. I call these HUDs. In a one-player game, player 2's HUD isn't displayed. In between the HUDs is the scanner. Above and below the HUDs/scanner area are a couple of horizontal lines. I call these the scanner border. The rest of the screen is the playfield. This is where ships, enemies, etc appear. Near the bottom of the playfield is the planet surface (unless you've managed to blow up the planet by losing all your humanoids). Soft Sprites ------------ I've barely started to tease apart the soft-sprite rendering code. What I do know: - Sprites are definitely used for the player's ship, enemies, enemy shots, bomber's bombs, humanoids. - All sprites are defined in a 10 pixel wide grid. These are 4-color-mode pixels (2bpp), so they're 20 bits wide. The left and right halves are drawn separately. Sprites that appear to be <= 5 pixels wide are generally the left halves (their right halves are blank). - When the ship (a full 10px wide sprite) collides with an enemy, it looks like only the left half of the enemy is drawn when it overlaps the ship. - Drawing is done by logical OR with whatever's already on the screen. - Collision checking isn't done by the drawing code. Not yet determined where this is done though. - The drawing routines (draw_sprite_left and draw_sprite_right) are copied to RAM, because they're self-modifying. The in-game text and players' scores aren't drawn by the soft-sprite code. Instead they've got their own separate renderers. The scanner and planet surface also have their own renderer(s) (of course, they look totally different from everything else). sprite_list is a 128-byte table, 32 entries, 4 bytes per entry: +0: ?? (0 = unused entry?) +1: ?? +2: ?? +3: type Sprite types are: 0 - humanoid 1 - lander 2 - mutant 3 - enemy shot (or bomber bomb?) 4 - bomber 5 - pod 6 - swarmer 7 - baiter There can never be more than 32 sprites in the world (not counting the player's ship). That's why sometimes on higher levels it's possible to shoot a pod and see no baiters (or only one) come out. XXX Am I 100% sure of this? sp_*, sprom_* format: Each pixel pair is used for one of the 4 color registers, as usual for ANTIC mode E. The colors are: 00 - COLBK - $00 (black) 01 - COLPF0 - $CA (green) 10 - COLPF1 - $38 (red) 11 - COLPF2 - $7E (white) Each sprite is either 10x8 pixels, split into two 5x8 left/right halves, or one 5x8 half-sprite. Each half is split into two 5x8 "characters" (not that we're using character-cell modes, I just need a name to call this). In the source, the half-sprites look like this (XXX not yet they don't): gfxrom_humanoid: .byte $28,$00 .byte $28,$00 .byte $14,$00 .byte $14,$00 .byte $14,$00 .byte $0C,$00 .byte $0C,$00 .byte $0C,$00 ...each pair of bytes is one row, or 5 pixels, packed into the top 10 bits of a 16-bit *MSB first* word. Notice the 2nd byte of each row is $00, because the humanoid fits in a 4x8 grid (actually he's only 2px wide). Most of the enemies are 5px wide, except the baiter. In other words, for each byte pair, the first byte is the left-most 4 pixels. The 2nd byte is the right-most 1 pixel, and always has 000000 in its bottom 6 bits [1]. Some of the sprites are animated (e.g. the player's ship, the pod). These are 2-frame animations. Each frame is a separate sprite. The sprite graphics in the ROM get labels like: sprom_humanoid (single-wide, non-animated sprite) sprom_lander_1 (single-wide, animated, this is frame 1, the other one will be called sprom_lander_2) sprom_baiter_l (double-wide, non-animated, this is the left half) sprom_lship_l_1 (double-wide, animated, this is the left half of frame 1) For the player's ship, there are 2 sets of sprites, one facing left and the other facing right. I'm calling these lship and rship. Also there's lflame and rflame for the ship's rocket exhaust. The game doesn't read the sprite data from ROM during gameplay. Instead, it copies it to RAM (in init_work_ram), then reads it from there. XXX not 100% sure why this is done or even if there's a reason. The RAM copies have the same labels as the ROM copies, but with sp_ instead of sprom_ as a prefix. [1] Patching the ROM to set these bits to something else sort-of works, but they aren't drawn always. This is because the drawing code only updates 2 bytes of screen memory (which is all that's needed for 10-bit wide sprite). With 11 or more bits, it's possible for them to be spread out over 3 bytes, which the engine doesn't handle. Text Rendering -------------- All text in the game is stored in the cartridge in an ASCII-like encoding, which I'll call DSCII here [1]. All character codes have their high bit set (so e.g. A is $41 in ASCII, $C1 in DSCII) [2]. The character set: $8D: carriage return (and newline) $A0: space $B0 - $B9: numbers 0 to 9 $C0: copyright symbol (in place of the @ from ASCII). prints double-wide. $C1 - $DA: alphabet A to Z (caps only, no lowercase in DSCII) Also, for the title screen text, draw_title_text supports escape codes to change the text color: $F0 - $FF: set glyph_color_mask. The mask gets set to NN where N is the low nybble (so $FA sets the mask to $AA, e.g.) The letters J, Q, and Z are never used in the game. Their glyphs in the font are backwards monochrome versions of the letters that look multicolored when rendered. This may be an artifact of the Apple II version, or just leftover data from an earlier version of the text-rendering code. The copyright symbol is actually 2 glyphs (left & right halves). The printing code checks for a valid character from the list above, so attempts to print any other character fail silently. Not that the game ever tries to do that anyway. All the strings of text in the game are null-terminated ($00 byte, like C). The subroutine that prints DSCII characters is at $972E. I've labelled it 'printchar'. It keeps track of its own cursor position (labelled cursor_x and cursor_y). [1] Defender Simplified Code, Internal Implementation :) [2] This is how characters are normally stored on the Apple II machines. I thought this might prove the Atari version was a port from Baker's unreleased Apple II version, but it turns out he used an Apple II as a development system while working on Defender at Atari. So even if the code was rewritten from scratch, it would make sense for it to use Apple-style ASCII codes. Scanner Rendering ----------------- Not looked into this yet. Sound ----- Audio channel 2 is used only for the sound of the player's engines. You can reproduce it in BASIC with: SOUND 1,31,8,2 This plays while the joystick is pressed left or right. When the joystick is released (or the player clears the level, or dies), channel 2 is muted, the equivalent of SOUND 1,0,0,0. Audio channels 3 and 4 are used only for the 'drone' you hear on the title screen and briefly at the start of each level. The sound is made by playing the two lowest notes on the two channels. It seems to rise and fall because of the beat frequency caused by the two notes being out of tune with each other. You can reproduce this in BASIC with: SOUND 2,255,10,15:SOUND 3,254,10,15 At the start/end of a level, the drone volume is steadily decreased until it reaches zero, so you hear the drone fade out. Audio channel 1 is used for event sounds, like explosions, distress calls, shots firing. This is everything except the 'drone' and engine noise. Each event sound has a priority, a tempo, and a list of AUDF1/AUDC1 values (which I'll call "steps"). The end of the list is marked by the volume bits (bits 0-3) in AUDC1 being all zero (silence). When the game wants to play a new sound and an old sound is still playing, the new sound will start to play if either: - its priority is equal or greater than the old sound's priority, - or the old sound has been playing for >=32 steps (regardless of tempo). When a new sound starts to play, any currently-playing one is replaced with the new one. The event sound engine is still active during the title screen, playing silence. You can test-play a sound (or a chunk of memory you suspect is a sound) from the atari800 debugger with e.g. "c b1 0 ll hh tt" where ll and hh are the low and high bytes of the sound's address and tt is the tempo (lower numbers = faster). Playing random chunks of memory won't hurt anything, usually makes "bump" or radio-static noises. The sound data tables in the code are named with a snd_ prefix. It turns out, each sound also has a specific subroutine that plays it. I've named these play_*. These all call a routine I've labelled play_event_sound. Once per jiffy, update_sound gets called. It sets the AUDC1/AUDF1 registers as needed. When the end of the current sound is reached, it queues up snd_silence (which will start playing next jiffy), so the sound engine is always playing something. Bugs ---- Defender is almost bug-free. There's one actual bug (in my opinion): - Sprites drawn at the right edge of the screen are distorted. Take a look at spritebug.png to see an example. There are a few things that people think of as bugs, which seem more like design limitations or compromises to me: - Too many objects on the screen causes slowdown. It's not much of a slowdown, but it's noticeable (maybe moreso on NTSC than PAL). - If your score reaches 10 million (8 digits), the rightmost digit flickers. This is just because there's only space for 7 digits, and the 8th is drawn in the same spot as the smartbombs in the HUD. Did anyone ever get 10 million points without cheating? - Sometimes when you shoot a pod, it doesn't spawn enough (or any) swarmers. This is due to the limit of 32 sprites: if you already had 30 when you shot the pod, you only get 2 swarmers. - The game gets stuck at level 99. Every time you beat level 99, the next level is also level 99. This isn't a bug because there's an explicit check in the code that causes it to happen. It's there because the code that prints the level number only knows how to print 2-digit numbers. Copy Protection --------------- Several places in the code check to see whether the game is being run from RAM instead of ROM: 1. init_cart checks the 2nd byte of RTCLOK to see how long the Atari has been running. RTCLOK+1 increments every time the RTCLOK+2 jiffy counter rolls over, which happens every 4.27 sec on NTSC (5.12 sec on PAL). This check detects whether the game was loaded from DOS, since it would take longer than that for DOS to boot and then load the game. If the check fails, bit 7 of protection_flag is set (see 3, below). 2. The routine at $8b8e (ram_self_destruct_2) is called during the main loop of the game. It attempts to write to ROM, then checks to see if the write succeeded. If it did, bit 7 of protection_flag is set, then the game jumps to the title screen (see 3, below). 3. When the user presses Start (from the title screen, or while the game is paused), start_game checks bit 7 of protection_flag. If it's set (due to checks 1 or 2 failing), it jumps back to the title screen instead of starting the game. 4. Another routine (ram_self_destruct_2 at $b47f) writes to ROM, replacing the DEC opcode at $983d with $02 (an invalid opcode, crashes the CPU when executed). The effect is that the game plays normally when running from RAM until the first time you get killed, which locks up the Atari. Tools ----- I used da65 from the cc65 suite for the disassembler, and the atari800 emulator's monitor for poking & prodding at the code to test theories. If anyone cares, all the text in these files was edited with vim. I wrote a few tools of my own: dumpfont.pl - dumps the font, uses ANSI color. Try piping into less +M. dumpgr.pl - dumps the graphics (sprites). dumptxt.c, dumpgttxt.c - dumps text strings from the ROM. 10 humanoids vs. 8 humanoids: ----------------------------- TL;DR version: 10 humanoids GOOD, 8 humanoids BAAAD! The obvious difference between the two is the number of humanoids you start out with. The version with 10 (I call this 10H) is what I had as a kid. It was a real cartridge made by Atari, bought for some outlandish price. The 8-humanoids (8H) version is almost identical. You'll notice at the end of a level where you still have all 8 humanoids, they're displayed off-center, with space on the right for 2 more, the same as they would be in the 10H version if you ended a level with 8 humanoids left. I remember finding pirated versions of Defender on BBSes back in the 300-baud days, and they were all the 8-humanoids version. I assumed this was an unofficial hack or a mistake made by whoever cracked the game... and now I'm 100% certain that I was right. The 8H image looks like a binary patch. Several places in the code are NOPped out, including a couple of places where the 10H code tries to write to ROM. The init code at $8000 is slightly different, and the table at $ae15 (which is DATA, not code) was modified (to $EA $EA, which are NOP opcodes also). Also see $ae17: "lda #$0a" was patched to "nop:asl a", which means someone went NOP-happy. This is where the number of humanoids get initialized ($0a = 10). Replacing with "asl a" is a bug... it so happens that the accumulator always holds 4 when this code runs, which gets left-shifted to 8... which is exactly how many humanoids this version of the game has. [Side note: with the 10H version, you can change the byte at $AE18 to change the number of starting humanoids. The game starts acting weird when you increase this too much. Also 0 means 256 here, not 0.] How did this happen? The table at $ae15 is 2 bytes of data, right in the middle of a section that's otherwise code. The disassemblers available on the Atari weren't very smart, and generally didn't give you much help separating code from data. Someone disassembled this sequence of bytes: AE15: 3E 20 A9 0A ...and it looked like this: ROL $A920 ; 3E 20 A9 ASL A ; 0A ...which looked to him like something that tried to modify ROM (since $A920 is in the ROM address space), so he replaced the ROL + its operand with NOPs. The correct disassembly looks like: .BYTE $3E,$20 ; 3E 20 LDA #$0A ; A9 0A The #$0A is the number of humanoids (10 in decimal). After the bad patch, knowing as we do that $AE15 is a data table, the code would look like: .BYTE $EA,$EA ; EA EA NOP ; EA ASL A ; 0A The operand of the LDA instruction is now an opcode, an ASL A. It so happens that when this code runs, the A register always has 4 in it. So it ends up as 8. I'm not (yet) sure what the $AE15 table is for, but changing it didn't make any obvious changes in gameplay. If it had caused the game to crash or display corrupted graphics, the cracker would have noticed that and gotten rid of this patch. Conclusion: the 8-humanoids version of the game started life as a dump of the 10-humanoids cartridge that someone hacked to get it to run from RAM without self-destructing. The NOPs are intended to get rid of the copy-protection code that makes it self-destruct if running from RAM... but whoever did this bungled the job, which caused the game to only have 8 humanoids. This was almost certainly done by an amateur cracker, and got circulated widely around the BBS scene as a binary load (xex) file and/or a bootdisk. Later on, someone found this hacked/cracked version and turned it back into a ROM image, which got included in the Holmes Archive. If someone out there owns a real Defender cartridge from Atari that has only 8 humanoids, my theory about the amateur cracker will be proved wrong, but that would just mean a professional programmer at Atari made this mistake instead of an amateur! ...it turns out someone else already figured this out 7 years ago. I couldn't find the answer by googling, so I disassembled the code and stared at it until I understood it well enough to write this explanation. Then I showed it to someone, and he pointed me towards Fandal's analysis: http://www.atarimania.com/atari_forum/viewtopic.php?f=1&t=2356 ...but, it wasn't a waste of my time, I learned a lot about the code in the process. Fandal also figured out the first change in the init code (to defeat the RTCLOK check), so I didn't have to... The 2nd change in the init code is to clear the coldstart flag, so the Reset key doesn't reboot the Atari. Diff of 10H vs. 8H disassemblies: --- defender.ca65 2017-09-16 15:16:41.984218123 -0400 +++ defender.8humans.ca65 2017-09-16 15:16:51.914217595 -0400 @@ -1,6 +1,6 @@ ; da65 V2.16 - Git 6de78c5 -; Created: 2017-09-16 15:16:41 -; Input file: defender.rom +; Created: 2017-09-16 15:16:51 +; Input file: defender.8humans.rom ; Page: 1 @@ -184,14 +184,14 @@ pla ; 8000 68 h pla ; 8001 68 h lsr protection_flag ; 8002 46 F8 F. - lda RTCLOK+1 ; 8004 A5 13 .. + lda #$00 ; 8004 A9 00 .. beq rtclok_ok ; 8006 F0 03 .. sec ; 8008 38 8 ror protection_flag ; 8009 66 F8 f. rtclok_ok: jsr init_hardware ; 800B 20 89 B8 .. jsr init_work_ram ; 800E 20 68 A2 h. - lda #$FF ; 8011 A9 FF .. + lda #$00 ; 8011 A9 00 .. sta COLDST ; 8013 8D 44 02 .D. lda #$F3 ; 8016 A9 F3 .. sta random_seed ; 8018 85 C6 .. @@ -1588,7 +1588,9 @@ lda table_b568 ; 8B8E AD 68 B5 .h. pha ; 8B91 48 H eor #$FF ; 8B92 49 FF I. - sta table_b568 ; 8B94 8D 68 B5 .h. + nop ; 8B94 EA . + nop ; 8B95 EA . + nop ; 8B96 EA . pla ; 8B97 68 h cmp table_b568 ; 8B98 CD 68 B5 .h. beq rom_ok ; 8B9B F0 06 .. @@ -5593,9 +5595,10 @@ ; ---------------------------------------------------------------------------- table_ae15: - .byte $3E,$20 ; AE15 3E 20 > + .byte $EA,$EA ; AE15 EA EA .. ; ---------------------------------------------------------------------------- -LAE17: lda #$0A ; AE17 A9 0A .. +LAE17: nop ; AE17 EA . + asl a ; AE18 0A . LAE19: pha ; AE19 48 H jsr get_random ; AE1A 20 F2 8C .. sta $DD ; AE1D 85 DD .. @@ -6517,7 +6520,9 @@ ; copy protection, causes game to crash next time the player gets killed. called from several places in the code. ram_self_destruct_1: lda #$02 ; B47F A9 02 .. - sta L983D ; B481 8D 3D 98 .=. + nop ; B481 EA . + nop ; B482 EA . + nop ; B483 EA . rts ; B484 60 ` ; ----------------------------------------------------------------------------