aboutsummaryrefslogtreecommitdiff
path: root/notes
blob: e2935350d66f870b3bed47e31148e043a6d7bb70 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
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                       `
 
 ; ----------------------------------------------------------------------------