diff options
author | B. Watson <yalhcru@gmail.com> | 2017-09-22 16:09:17 -0400 |
---|---|---|
committer | B. Watson <yalhcru@gmail.com> | 2017-09-22 16:09:17 -0400 |
commit | b2cad8050e2077f09535607a829183a842023da7 (patch) | |
tree | 1f51d630c7596355b023a8cb4583fe46e3d066a7 /notes | |
download | defender-b2cad8050e2077f09535607a829183a842023da7.tar.gz |
initial commit
Diffstat (limited to 'notes')
-rw-r--r-- | notes | 562 |
1 files changed, 562 insertions, 0 deletions
@@ -0,0 +1,562 @@ +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 ` + + ; ---------------------------------------------------------------------------- |