aboutsummaryrefslogtreecommitdiff
path: root/notes
diff options
context:
space:
mode:
authorB. Watson <yalhcru@gmail.com>2017-09-22 16:09:17 -0400
committerB. Watson <yalhcru@gmail.com>2017-09-22 16:09:17 -0400
commitb2cad8050e2077f09535607a829183a842023da7 (patch)
tree1f51d630c7596355b023a8cb4583fe46e3d066a7 /notes
downloaddefender-b2cad8050e2077f09535607a829183a842023da7.tar.gz
initial commit
Diffstat (limited to 'notes')
-rw-r--r--notes562
1 files changed, 562 insertions, 0 deletions
diff --git a/notes b/notes
new file mode 100644
index 0000000..e293535
--- /dev/null
+++ b/notes
@@ -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 `
+
+ ; ----------------------------------------------------------------------------