--------------Lode Runner-------------- A 4am crack 2018-10-20 -------------------. updated 2023-08-01 |___________________ Name: Lode Runner Genre: arcade Year: 1983 Credits: Doug Smith Publisher: Broderbund Software Platform: Apple ][+ or later Media: 5.25-inch disk Sides: 1 OS: custom Similar cracks: #280 Championship Lode Runner ~ Chapter 0 In Which Various Automated Tools Fail In Interesting Ways COPYA immediate disk read error Locksmith Fast Disk Backup unable to read any track EDD 4 bit copy (no sync, no count) read errors on tracks $0E, $11, $14, $17, $1A, $1D, $20, and $22 copy just hangs on boot Copy ][+ nibble editor T03-T0C appear to be mostly normal with modified address epilogue Some higher tracks appear to have 4-4 encoded nibbles Disk Fixer ["O" -> "Input/Output Control"] set CHECKSUM ENABLED=NO T03-T0C readable Why didn't COPYA work? so many reasons Why didn't Locksmith FDB work? modified epilogues on some tracks, non-sector-based data on others Why didn't my EDD copy work? Looking through old bit copy parameter files, they claim that on tracks $0D and higher, data is stored on quarter tracks every 1.5 tracks ($0D.25, $0E.75, &c.) Even with these parameters, I was unable to create a working protected backup of my original disk. This is going to be one of those "capture the game in memory and rebuild it from the ground up" cracks. Next steps: 1. Trace bootloader 2. Capture game code in memory 3. Write game to a standard disk and build a bootloader to load it 4. Declare victory (*) (*) go to the gym ~ Chapter 1 In Which We Brag About Our Humble Beginnings I have two floppy drives, one in slot 6 and the other in slot 5. My "work disk" (in slot 5) runs Diversi-DOS 64K, which is compatible with Apple DOS 3.3 but relocates most of DOS to the language card on boot. This frees up most of main memory (only using a single page at $BF00..$BFFF), which is useful for loading large files or examining code that lives in areas typically reserved for DOS. [S6,D1=original disk] [S5,D1=my work disk] The floppy drive firmware code at $C600 is responsible for aligning the drive head and reading sector 0 of track 0 into main memory at $0800. Because the drive can be connected to any slot, the firmware code can't assume it's loaded at $C600. If the floppy drive card were removed from slot 6 and reinstalled in slot 5, the firmware code would load at $C500 instead. To accommodate this, the firmware does some fancy stack manipulation to detect where it is in memory (which is a neat trick, since the 6502 program counter is not generally accessible). However, due to space constraints, the detection code only cares about the lower 4 bits of the high byte of its own address. Stay with me, this is all about to come together and go boom. $C600 (or $C500, or anywhere in $Cx00) is read-only memory. I can't change it, which means I can't stop it from transferring control to the boot sector of the disk once it's in memory. BUT! The disk firmware code works unmodified at any address. Any address that ends with $x600 will boot slot 6, including $B600, $A600, $9600, &c. ; copy drive firmware to $9600 *9600 at the title screen and you can initialize your own data disk and build and save your own levels.) 244A- EA NOP 244B- 20 80 04 JSR $0480 *2480L ; tell the DOS-shaped RWTS that we're ; on track $0C 2480- 8A TXA 2481- 4A LSR 2482- 4A LSR 2483- 4A LSR 2484- 4A LSR 2485- AA TAX 2486- A9 18 LDA #$18 2488- 9D 78 04 STA $0478,X ; use the bootloader track seek routine ; to actually seek to track $0C (normal ; timing) 248B- 4C 03 06 JMP $0603 This prevents the disk from grinding when we switch over to the DOS-shaped RWTS to read level 1. ; game-specific zero page setup, which ; I'm assuming is important 244E- EA NOP 244F- EA NOP 2450- A9 06 LDA #$06 2452- 85 8C STA $8C 2454- A9 FF LDA #$FF 2456- 85 99 STA $99 2458- A9 CA LDA #$CA 245A- 85 95 STA $95 245C- A9 4C LDA #$4C 245E- 85 23 STA $23 2460- A9 50 LDA #$50 2462- 85 36 STA $36 2464- A9 8E LDA #$8E 2466- 85 37 STA $37 ; ha ha, Roland loves using the I/O ; vector to point to the RWTS entry ; point, so he can "print" a character ; to read a sector 2468- A9 B5 LDA #$B5 246A- 85 38 STA $38 246C- A9 B7 LDA #$B7 246E- 85 39 STA $39 ; this is the *actual* game entry point 2470- 4C 00 60 JMP $6000 ~ Chapter 7 If You Wish To Play A Game, You Must First Create The Universe So far we've focused on the game code and ignored the level data. Now that we have all the game code (finally!) we can use Advanced Demuffin to capture the level data on tracks $03-$0C. The RWTS to read the level data is at $B800, which I captured as earlier as part of the "OBJ.A000-BFFF" file. ]BLOAD OBJ.A000-BFFF,A$4000 ]BSAVE RWTS,A$5800,L$800 Now let's run Advanced Demuffin to copy those tracks to a standard format. ]BRUN ADVANCED DEMUFFIN 1.5 [S6,D1=original disk] [S6,D2=formatted blank disk] [S5,D1=my work disk (still)] ["5" to switch to slot 5] ["R" to load a new RWTS module] --> At $B8, load "RWTS" from drive 1 ["6" to switch to slot 6] ["C" to convert disk] [press "Y" to change default values] --v-- ADVANCED DEMUFFIN 1.5 (C) 1983, 2014 ORIGINAL BY THE STACK UPDATES BY 4AM ======================================= INPUT ALL VALUES IN HEX SECTORS PER TRACK? (13/16) 16 START TRACK: $03 <-- change this START SECTOR: $00 END TRACK: $0C <-- change this END SECTOR: $0F INCREMENT: 1 MAX # OF RETRIES: 0 COPY FROM DRIVE 1 TO DRIVE: 2 ======================================= 16SC $03,$00-$0C,$0F BY$01 S6,D1->S6,D2 --^-- And here we go... --v-- ADVANCED DEMUFFIN 1.5 (C) 1983, 2014 ORIGINAL BY THE STACK UPDATES BY 4AM =======PRESS ANY KEY TO CONTINUE======= TRK: .......... +.5: 0123456789ABCDEF0123456789ABCDEF012 SC0: .......... SC1: .......... SC2: .......... SC3: .......... SC4: .......... SC5: .......... SC6: .......... SC7: .......... SC8: .......... SC9: .......... SCA: .......... SCB: .......... SCC: .......... SCD: .......... SCE: .......... SCF: .......... ======================================= 16SC $03,$00-$0C,$0F BY$01 S6,D1->S6,D2 --^-- Then I took a bog standard RWTS from a freshly initialized DOS 3.3 disk, to replace the mostly-but-not-actually- standard RWTS that the game loads at $B800. [S6,D1=DOS 3.3 master disk] [S5,D1=my work disk] ]PR#6 ... ]CALL -151 *2800 $EC ; (to be used later) 0816- 09 8C ORA #$8C ; Copy the boot1 code from $0901..$09FF ; to zero page. ($0900 holds the 0boot ; version number. This is version 3. ; $0000 is initialized later in boot1.) 0818- BE 00 09 LDX $0900,Y 081B- 96 00 STX $00,Y 081D- C8 INY 081E- D0 F8 BNE $0818 ; There are a number of places in boot1 ; that need to hit a slot-specific soft ; switch (read a nibble from disk, turn ; off the drive, &c). Rather than the ; usual form of "LDA $C08C,X", we will ; use "LDA $C0EC" and modify the $EC ; byte in advance, based on the boot ; slot. $00E4 is an array of all the ; places in the boot1 code that need ; this adjustment. 0820- C8 INY 0821- B6 E4 LDX $E4,Y 0823- 95 00 STA $00,X 0825- D0 F9 BNE $0820 ; munge $EC -> $E0 (used later to ; advance the drive head to the next ; track) 0827- 29 F0 AND #$F0 0829- 85 CB STA $CB ; munge $E0 -> $E8 (used later to ; turn off the drive motor) 082B- 09 08 ORA #$08 082D- 85 D3 STA $D3 ; push sector interleave array to the ; bottom of the stack (by setting the ; stack pointer to #$0F and pushing ; #$10 bytes, those bytes will end up ; in $0100..$010F) 082F- A2 0F LDX #$0F 0831- 9A TXS 0832- BD B5 08 LDA $08B5,X 0835- 48 PHA 0836- CA DEX 0837- 10 F9 BPL $0832 For reference, this is the sector interleave array: 08B5- .. .. .. .. .. 00 07 0E 08B8- 06 0D 05 0C 04 0B 03 0A 08C0- 02 09 01 08 0F ; push several addresses to the ; stack (more on this later) 0839- A9 08 LDA #$08 083B- 48 PHA 083C- A9 9C LDA #$9C 083E- 48 PHA 083F- A2 06 LDX #$06 0841- B5 DE LDA $DE,X 0843- 48 PHA 0844- CA DEX 0845- D0 FA BNE $0841 ; number of tracks to load (x2) (game- ; specific -- this game uses 7 tracks) 0847- A0 0E LDY #$0E ; loop starts here 0849- 8A TXA ; the carry was set by the "LSR" at ; $0801, so we won't take this branch ; the first time (but, as we will see ; shortly, the carry gets flipped off ; and on, and we end up taking this ; branch every second time through the ; loop) 084A- 90 0F BCC $085B ; check if we want to change the target ; address to store the track data 084C- B9 C3 08 LDA $08C3,Y ; 0 = no change (each track is stored ; in the next $1000 bytes in memory ; unless we change it) 084F- F0 07 BEQ $0858 0851- 48 PHA ; X is #$00 going into this loop, and ; it never changes, so now A is #$00. 0852- 8A TXA ; Push $00DA to the stack, which (when ; we pop it from the stack later) will ; "return" to $00DB. That routine sets ; the target address to store the data ; from the next track we read. 0853- 48 PHA 0854- A9 DA LDA #$DA 0856- 48 PHA ; Push $0000 to the stack to "return" ; to $0001, which reads a track into ; memory 0857- 8A TXA ; Note that execution ends up here ; regardless -- either we fell through ; from $0857 or we branched from $084F. ; Either way, A is #$00. 0858- 48 PHA 0859- 48 PHA ; There's a "SEC" hidden here (because ; it's opcode $38), but it's only ; executed if we take the branch at ; $084A, which lands at $085B, which is ; in the middle of this instruction. ; Otherwise we execute the compare, ; which clears the carry bit because A ; is always #$00 at this point. So the ; carry flip-flops between set and ; clear, so the BCC at $084A is only ; taken every other time. 085A- C9 38 CMP #$38 ; Push $00B6 to the stack, to "return" ; to $00B7. This routine advances the ; drive head to the next half track. 085C- 48 PHA 085D- A9 B6 LDA #$B6 085F- 48 PHA ; loop until done 0860- 88 DEY 0861- D0 E6 BNE $0849 Because of the carry flip-flop, we will push $00B6 to the stack every time through the loop, but we will only push $0000 every other time. Occasionally we also push a byte for the new target address and the address of the routine that changes it. ; push $00B6 (track advance) to the ; stack $18 times, because the game ; code actually starts on track $0D ; because tracks $03-$0C are level data 0863- A0 18 LDY #$18 0865- 8A TXA 0866- 48 PHA 0867- A9 B6 LDA #$B6 0869- 48 PHA 086A- 88 DEY 086B- D0 F8 BNE $0865 The final stack looks like this: --top-- $00B6 (move to track $00.5) $00B6 (move to track $01) . . [repeated] . $00B6 (move drive to track $0C.5) $00B6 (move drive to track $0D) $0000 (read track into $0F00..$1EFF) $00B6 \ $00B6 \ move to track $0E, $00DA } set target address $60 / to $6000, $0000 / and read entire track $00B6 \ $00B6 } move to T0F, read into $7000 $0000 / . . [repeated for each track] . $00B6 \ $00B6 } move to T13, read into $B000 $0000 / $FE88 (IN#0, pushed at $0841) $FE92 (PR#0, pushed at $0841) $00D1 (turn off drive motor) $089C (final setup, pushed at $0839) . . [unused] . $00070E060D050C040B030A020901080F (sector interleave table) --bottom-- Boot1 reads the game into memory from tracks $0D-$13, but it isn't a loop. One routine advances the drive head, another routine reads a track, and a third routine changes the address to store data in memory. We're essentially unrolling the read loop on the stack in advance. Each routine gets called when we need it, as many times we need it. Like dancers in a chorus line, each routine does its dance then cedes the spotlight. Each seems unaware of the others, but in reality they've all been meticulously choreographed. ~ Chapter 9 6 + 2 Before I can explain the next chunk of code, I need to pause and explain a little bit of theory. As you probably know if you're the sort of person who reads this sort of thing, Apple II floppy disks do not contain the actual data that ends up being loaded into memory. Due to hardware limitations of the original Disk II drive, data on disk must be stored in an intermediate format called "nibbles." Bytes in memory are encoded into nibbles before writing to disk, and nibbles that you read from the disk must be decoded back into bytes. The round trip is lossless but requires some bit wrangling. Decoding nibbles-on-disk into bytes-in- memory is a multi-step process. In "6-and-2 encoding" (used by DOS 3.3, ProDOS, and all ".dsk" image files), there are 64 possible values that you may find in the data field (in the range $96..$FF, but not all of those, because some of them have bit patterns that trip up the drive firmware). We'll call these "raw nibbles." Step 1: read $156 raw nibbles from the data field. These values will range from $96 to $FF, but as mentioned earlier, not all values in that range will appear on disk. Now we have $156 raw nibbles. Step 2: decode each of the raw nibbles into a 6-bit byte between 0 and 63 (%00000000 and %00111111 in binary). $96 is the lowest valid raw nibble, so it gets decoded to 0. $97 is the next valid raw nibble, so it's decoded to 1. $98 and $99 are invalid, so we skip them, and $9A gets decoded to 2. And so on, up to $FF (the highest valid raw nibble), which gets decoded to 63. Now we have $156 6-bit bytes. Step 3: split up each of the first $56 6-bit bytes into pairs of bits. In other words, each 6-bit byte becomes three 2-bit bytes. These 2-bit bytes are merged with the next $100 6-bit bytes to create $100 8-bit bytes. Hence the name, "6-and-2" encoding. The exact process of how the bits are split and merged is... complicated. The first $56 6-bit bytes get split up into 2-bit bytes, but those two bits get swapped (so %01 becomes %10 and vice- versa). The other $100 6-bit bytes each get multiplied by 4 (a.k.a. bit-shifted two places left). This leaves a hole in the lower two bits, which is filled by one of the 2-bit bytes from the first group. A diagram might help. "a" through "x" each represent one bit. ------------- 1 decoded 3 decoded nibble in + nibbles in = 3 bytes first $56 other $100 00abcdef 00ghijkl 00mnopqr | 00stuvwx | split | & shifted swapped left x2 | | V V 000000fe + ghijkl00 = ghijklfe 000000dc + mnopqr00 = mnopqrdc 000000ba + stuvwx00 = stuvwxba ------------- Tada! Four 6-bit bytes 00abcdef 00ghijkl 00mnopqr 00stuvwx become three 8-bit bytes ghijklfe mnopqrdc stuvwxba When DOS 3.3 reads a sector, it reads the first $56 raw nibbles, decoded them into 6-bit bytes, and stashes them in a temporary buffer (at $BC00). Then it reads the other $100 raw nibbles, decodes them into 6-bit bytes, and puts them in another temporary buffer (at $BB00). Only then does DOS 3.3 start combining the bits from each group to create the full 8-bit bytes that will end up in the target page in memory. This is why DOS 3.3 "misses" sectors when it's reading, because it's busy twiddling bits while the disk is still spinning. ~ Chapter 10 Shift Happens 0boot also uses "6-and-2" encoding. The first $56 nibbles in the data field are still split into pairs of bits that need to be merged with nibbles that won't come until later. But instead of waiting for all $156 raw nibbles to be read from disk, it "interleaves" the nibble reads with the bit twiddling required to merge the first $56 6-bit bytes and the $100 that follow. By the time 0boot gets to the data field checksum, it has already stored all $100 8-bit bytes in their final resting place in memory. This means that 0boot can read all 16 sectors on a track in one revolution of the disk. That's crazy fast. To make it possible to do all the bit twiddling we need to do and not miss nibbles as the disk spins(*), we do some of the work earlier. We multiply each of the 64 possible decoded values by 4 and store those values. (Since this is accomplished by bit shifting and we're doing it before we start reading the disk, this is called the "pre-shift" table.) We also store all possible 2-bit values in a repeating pattern that will make it easy to look them up later. Then, as we're reading from disk (and timing is tight), we can simulate all the bit math we need to do with a series of table lookups. There is just enough time to convert each raw nibble into its final 8-bit byte before reading the next nibble. (*) The disk spins independently of the CPU, and we only have a limited time to read a nibble and do what we're going to do with it before WHOOPS HERE COMES ANOTHER ONE. So time is of the essence. Also, "As The Disk Spins" would make a great name for a retrocomputing-themed soap opera. The first table, at $0200..$02FF, is three columns wide and 64 rows deep. Astute readers will notice that 3 x 64 is not 256. Only three of the columns are used; the fourth (unused) column exists because multiplying by 3 is hard but multiplying by 4 is easy (in base 2 anyway). The three columns correspond to the three pairs of 2-bit values in those first $56 6-bit bytes. Since the values are only 2 bits wide, each column holds one of four different values (%00, %01, %10, or %11). The second table, at $036C..$03D5, is the "pre-shift" table. This contains all the possible 6-bit bytes, in order, each multiplied by 4 (a.k.a. shifted to the left two places, so the 6 bits that started in columns 0-5 are now in columns 2-7, and columns 0 and 1 are zeroes). Like this: 00ghijkl --> ghijkl00 Astute readers will notice that there are only 64 possible 6-bit bytes, but this second table is larger than 64 bytes. To make lookups easier, the table has empty slots for each of the invalid raw nibbles. In other words, we don't do any math to decode raw nibbles into 6-bit bytes; we just look them up in this table (offset by $96, since that's the lowest valid raw nibble) and get the required bit shifting for free. addr | raw | decoded 6-bit | pre-shift -----+-----+----------------+---------- $36C | $96 | 0 = %00000000 | %00000000 $36D | $97 | 1 = %00000001 | %00000100 $36E | $98 [invalid raw nibble] $36F | $99 [invalid raw nibble] $370 | $9A | 2 = %00000010 | %00001000 $371 | $9B | 3 = %00000011 | %00001100 $372 | $9C [invalid raw nibble] $373 | $9D | 4 = %00000100 | %00010000 . . . $3D4 | $FE | 62 = %00111110 | %11111000 $3D5 | $FF | 63 = %00111111 | %11111100 Each value in this "pre-shift" table also serves as an index into the first table (with all the 2-bit bytes). This wasn't an accident; I mean, that sort of magic doesn't just happen. But the table of 2-bit bytes is arranged in such a way that we take one of the raw nibbles that needs to be decoded and split apart (from the first $56 raw nibbles in the data field), use that raw nibble as an index into the pre- shift table, then use that pre-shifted value as an index into the first table to get the 2-bit value we need. That's a neat trick. ; this loop creates the pre-shift table ; at $36C 086D- A2 6A LDX #$6A 086F- 1E 6B 03 ASL $036B,X 0872- 1E 6B 03 ASL $036B,X 0875- CA DEX 0876- D0 F7 BNE $086F Wait, what? It turns out the drive firmware already creates a table that looks very similar to the pre-shift table we want... it's just not shifted yet! Since we're not calling the drive firmware anymore, we can take full advantage of this table that's guaranteed to be in memory. And this is the result (".." means the address is unused): 036C- 00 04 .. .. 0370- 08 0C .. 10 14 18 .. .. 0378- .. .. .. .. 1C 20 .. .. 0380- .. 24 28 2C 30 34 .. .. 0388- 38 3C 40 44 48 4C .. 50 0390- 54 58 5C 60 64 68 .. .. 0398- .. .. .. .. .. .. .. .. 03A0- .. 6C .. 70 74 78 .. .. 03A8- .. 7C .. .. 80 84 .. 88 03B0- 8C 90 94 98 9C A0 .. .. 03B8- .. .. .. A4 A8 AC .. B0 03C0- B4 B8 BC C0 C4 C8 .. .. 03C8- CC D0 D4 D8 DC E0 .. E4 03D0- E8 EC F0 F4 F8 FC ; this loop creates the table of 2-bit ; values at $200, magically arranged to ; enable easy lookups later 0878- C8 INY 0879- 46 BA LSR $BA 087B- 46 BA LSR $BA 087D- B5 EB LDA $EB,X 087F- 99 FF 01 STA $01FF,Y 0882- E6 AF INC $AF 0884- A5 AF LDA $AF 0886- 25 BA AND $BA 0888- D0 05 BNE $088F 088A- E8 INX 088B- 8A TXA 088C- 29 03 AND #$03 088E- AA TAX 088F- C8 INY 0890- C8 INY 0891- C8 INY 0892- C8 INY 0893- C0 04 CPY #$04 0895- B0 E6 BCS $087D 0897- C8 INY 0898- C0 04 CPY #$04 089A- 90 DD BCC $0879 And this is the result: 0200- 00 00 00 .. 00 00 02 .. 0208- 00 00 01 .. 00 00 03 .. 0210- 00 02 00 .. 00 02 02 .. 0218- 00 02 01 .. 00 02 03 .. 0220- 00 01 00 .. 00 01 02 .. 0228- 00 01 01 .. 00 01 03 .. 0230- 00 03 00 .. 00 03 02 .. 0238- 00 03 01 .. 00 03 03 .. 0240- 02 00 00 .. 02 00 02 .. 0248- 02 00 01 .. 02 00 03 .. 0250- 02 02 00 .. 02 02 02 .. 0258- 02 02 01 .. 02 02 03 .. 0260- 02 01 00 .. 02 01 02 .. 0268- 02 01 01 .. 02 01 03 .. 0270- 02 03 00 .. 02 03 02 .. 0278- 02 03 01 .. 02 03 03 .. 0280- 01 00 00 .. 01 00 02 .. 0288- 01 00 01 .. 01 00 03 .. 0290- 01 02 00 .. 01 02 02 .. 0298- 01 02 01 .. 01 02 03 .. 02A0- 01 01 00 .. 01 01 02 .. 02A8- 01 01 01 .. 01 01 03 .. 02B0- 01 03 00 .. 01 03 02 .. 02B8- 01 03 01 .. 01 03 03 .. 02C0- 03 00 00 .. 03 00 02 .. 02C8- 03 00 01 .. 03 00 03 .. 02D0- 03 02 00 .. 03 02 02 .. 02D8- 03 02 01 .. 03 02 03 .. 02E0- 03 01 00 .. 03 01 02 .. 02E8- 03 01 01 .. 03 01 03 .. 02F0- 03 03 00 .. 03 03 02 .. 02F8- 03 03 01 .. 03 03 03 .. ; And that's all she wrote. Everything ; else is already lined up on the ; stack. All that's left to do is ; "return" and let the stack guide us ; through the rest of the boot. 089C- 60 RTS [Note to future self: $089C..$08FF is available for game-specific init code, but it can't rely on or disturb zero page in any way. That rules out a lot of built-in ROM routines; be careful.] ~ Chapter 11 0boot boot1 The rest of the boot runs from zero page. It's hard to show you exactly what boot1 will look like, because it relies heavily on self-modifying code. In a standard DOS 3.3 RWTS, the softswitch to read the data latch is "LDA $C08C,X", where X is the boot slot times 16 (to allow disks to boot from any slot). 0boot also supports booting from any slot, but instead of using an index, each fetch instruction is pre- set based on the boot slot. Not only does this free up the X register, it lets us juggle all the registers and put the raw nibble value in whichever one is convenient at the time. (We take full advantage of this freedom.) I've marked each pre-set softswitch with "o_O" to remind you that self-modifying code is awesome. There are several other instances of addresses and constants that get modified while boot1 is running. I've marked these with "/!\" to remind you that self-modifying code is dangerous and you should not try this at home. The first thing popped off the stack is the drive arm move routine at $00B7. It moves the drive exactly one phase (half a track). 00B7- E6 BA INC $BA ; This value was set at $00B7 (above). ; It's incremented monotonically, but ; it's ANDed with $03 later, so its ; exact value isn't relevant. 00B9- A0 3F LDY #$00 /!\ ; short wait for PHASEON 00BB- A9 04 LDA #$04 00BD- 20 C3 00 JSR $00C3 ; fall through 00C0- 88 DEY ; longer wait for PHASEOFF 00C1- 69 41 ADC #$41 00C3- 85 CE STA $CE ; calculate the proper stepper motor to ; access 00C5- 98 TYA 00C6- 29 03 AND #$03 00C8- 2A ROL 00C9- AA TAX ; This address was set at $0827, ; based on the boot slot. 00CA- BD E0 C0 LDA $C0E0,X /!\ ; This value was set at $00C3 so that ; PHASEON and PHASEOFF have optimal ; wait times. 00CD- A9 D1 LDA #$D1 /!\ ; wait exactly the right amount of time ; after accessing the proper stepper ; motor 00CF- 4C A8 FC JMP $FCA8 Since the drive arm routine only moves one phase, it was pushed to the stack twice before each track read. Our game is stored on whole tracks; this half- track trickery is only to save a few bytes of code in boot1. (Hey, we're on zero page; space is tight!) The track read routine starts at $0001, because that let us save 1 byte in the boot0 code when we were pushing addresses to the stack. (We could just push $00 twice.) ; sectors-left-to-read-on-this-track ; counter (incremented to $00) 0001- A2 F0 LDX #$F0 0003- 86 00 STX $00 We initialize an array at $00DF that tracks which sectors we've read from the current track. Astute readers will notice that this part of zero page had real data in it -- some addresses that were pushed to the stack, and some other values that were used to create the 2-bit table at $0200. All true, but all those operations are now complete, and the space is now available for unrelated uses. The array is in logical sector order; we convert physical to logical sectors immediately after reading the address field. Values are the actual pages in memory where that sector should go, and they get zeroed once the sector is read (so we don't waste time decoding the same sector twice). ; starting address (game-specific; ; this one starts loading at $0F00) 0005- A9 0F LDA #$0F /!\ 0007- 95 EF STA $EF,X 0009- E6 06 INC $06 000B- E8 INX 000C- D0 F7 BNE $0005 000E- 20 D5 00 JSR $00D5 ; subroutine reads a nibble and ; stores it in the accumulator 00D5- AD EC C0 LDA $C0EC o_O 00D8- 10 FB BPL $00D5 00DA- 60 RTS Continuing from $0011... ; first nibble must be $D5 0011- C9 D5 CMP #$D5 0013- D0 F9 BNE $000E ; read second nibble, must be $AA 0015- 20 D5 00 JSR $00D5 0018- C9 AA CMP #$AA 001A- D0 F5 BNE $0011 ; We actually need the Y register to be ; $AA for unrelated reasons later, so ; let's set that now. (We have time, ; and it saves 1 byte!) 001C- A8 TAY ; read the third nibble 001D- 20 D5 00 JSR $00D5 ; is it $AD? 0020- 49 AD EOR #$AD ; Yes, which means this is the data ; prologue. Branch forward to start ; reading the data field. 0022- F0 22 BEQ $0046 If that third nibble is not $AD, we assume it's the end of the address prologue. ($96 would be the third nibble of a standard address prologue, but we don't actually check.) We fall through and start decoding the 4-4 encoded values in the address field. 0024- A0 02 LDY #$02 The first time through this loop, we'll read the disk volume number. The second time, we'll read the track number. The third time, we'll read the physical sector number. We don't actually care about the disk volume or the track number, and once we get the sector number, we don't verify the address field checksum. 0026- 20 D5 00 JSR $00D5 0029- 2A ROL 002A- 85 AF STA $AF 002C- 20 D5 00 JSR $00D5 002F- 25 AF AND $AF 0031- 88 DEY 0032- 10 F2 BPL $0026 ; take physical sector number (in A) ; and use it to look up the logical ; sector number 0034- AA TAX 0035- BC 00 01 LDY $0100,X ; store logical sector number 0038- 84 AF STY $AF ; use logical sector number as an ; index into the sector address array ; to get the target page (where we want ; to store this sector in memory) 003A- B6 DF LDX $DF,Y ; store the target page in several ; places throughout the following code 003C- 86 9E STX $9E 003E- CA DEX 003F- 86 6E STX $6E 0041- 86 86 STX $86 0043- E8 INX ; This is an unconditional branch, ; because the ROL at $0029 will always ; set the carry. We're done processing ; the address field, so we need to loop ; back and wait for the data prologue. 0044- B0 C8 BCS $000E ; execution continues here (from $0022) ; after matching the data prologue 0046- E0 00 CPX #$00 ; If X is still $00, it means we found ; a data prologue before we found an ; address prologue. In that case, we ; have to skip this sector, because we ; don't know which sector it is and we ; wouldn't know where to put it. 0048- F0 C4 BEQ $000E Nibble loop #1 reads nibbles $00..$55, looks up the corresponding offset in the preshift table at $036C, and stores that offset in the temporary buffer at $0300. ; initialize rolling checksum to $00 004A- 85 58 STA $58 004C- AE EC C0 LDX $C0EC o_O 004F- 10 FB BPL $004C ; The nibble value is in the X register ; now. The lowest possible nibble value ; is $96 and the highest is $FF. To ; look up the offset in the table at ; $036C, we need to subtract $96 from ; $036C and add X. 0051- BD D6 02 LDA $02D6,X ; Now the accumulator has the offset ; into the table of individual 2-bit ; combinations ($0200..$02FF). Store ; that offset in the temporary buffer ; at $0300, in the order we read the ; nibbles. But the Y register started ; counting at $AA, so we need to ; subtract $AA from $0300 and add Y. 0054- 99 56 02 STA $0256,Y ; The EOR value is set at $004A ; each time through loop #1. 0057- 49 00 EOR #$00 /!\ 0059- C8 INY 005A- D0 EE BNE $004A Here endeth nibble loop #1. Nibble loop #2 reads nibbles $56..$AB, combines them with bits 0-1 of the appropriate nibble from the first $56, and stores them in bytes $00..$55 of the target page in memory. 005C- A0 AA LDY #$AA 005E- AE EC C0 LDX $C0EC o_O 0061- 10 FB BPL $005E 0063- 5D D6 02 EOR $02D6,X 0066- BE 56 02 LDX $0256,Y 0069- 5D 02 02 EOR $0202,X ; This address was set at $003F ; based on the target page (minus 1 ; so we can add Y from $AA..$FF). 006C- 99 56 D1 STA $D156,Y /!\ 006F- C8 INY 0070- D0 EC BNE $005E Here endeth nibble loop #2. Nibble loop #3 reads nibbles $AC..$101, combines them with bits 2-3 of the appropriate nibble from the first $56, and stores them in bytes $56..$AB of the target page in memory. 0072- 29 FC AND #$FC 0074- A0 AA LDY #$AA 0076- AE EC C0 LDX $C0EC o_O 0079- 10 FB BPL $0076 007B- 5D D6 02 EOR $02D6,X 007E- BE 56 02 LDX $0256,Y 0081- 5D 01 02 EOR $0201,X ; This address was set at $0041 ; based on the target page (minus 1 ; so we can add Y from $AA..$FF). 0084- 99 AC D1 STA $D1AC,Y /!\ 0087- C8 INY 0088- D0 EC BNE $0076 Here endeth nibble loop #3. Loop #4 reads nibbles $102..$155, combines them with bits 4-5 of the appropriate nibble from the first $56, and stores them in bytes $AC..$FF of the target page in memory. 008A- 29 FC AND #$FC 008C- A2 AC LDX #$AC 008E- AC EC C0 LDY $C0EC o_O 0091- 10 FB BPL $008E 0093- 59 D6 02 EOR $02D6,Y 0096- BC 54 02 LDY $0254,X 0099- 59 00 02 EOR $0200,Y ; This address was set at $003C ; based on the target page. 009C- 9D 00 D1 STA $D100,X /!\ 009F- E8 INX 00A0- D0 EC BNE $008E Here endeth nibble loop #4. ; Finally, get the last nibble, ; which is the checksum of all ; the previous nibbles. 00A2- 29 FC AND #$FC 00A4- AC EC C0 LDY $C0EC o_O 00A7- 10 FB BPL $00A4 00A9- 59 D6 02 EOR $02D6,Y ; If checksum fails, start over. ; Note: we really want to branch ; to $000E, but that's too far, ; so we're branching to an earlier ; unrelated "BCS" which branches ; to $000E. The carry is always ; set at this point (it was set ; by the "CPX #$00" all the way ; back at $0046), so the BCS is ; an unconditional jump and we ; end up where we want (at $000E). 00AC- D0 96 BNE $0044 ; This was set to the logical ; sector number (at $0038), so ; this is a index into the 16- ; byte array at $00DF. 00AE- A0 00 LDY #$00 /!\ ; store #$00 at this index in the ; sector array to indicate that ; we've read this sector 00B0- 96 DF STX $DF,Y ; are we done yet? 00B2- E6 00 INC $00 ; nope, loop back to read more sectors 00B4- D0 8E BNE $0044 ; And that's all she read. 00B6- 60 RTS 0boot's track read routine is done when $0000 hits $00, which is astonishingly beautiful. Like, "now I know God" level of beauty. And so it goes: we pop another address off the stack, move the drive arm, read another track, and so on. Eventually we finish moving and reading, moving and reading, and we get to the home stretch and start calling ROM routines. $FE88 (IN#0, pushed at $0841) $FE92 (PR#0, pushed at $0841) Next on the stack: $00D1 (turn off drive motor) 00D2- AD E8 C0 LDA $C0E8 /!\ Note that this routine falls through to the one at $00D5 which reads a nibble from disk, but that's harmless. And the last thing on the stack: $089C (final setup, pushed at $0839) ...which jumps to $089D, which was part of track 0, sector 0. ; this boot slot was modified ; earlier (at $0813) 089D- A9 60 LDA #$60 /!\ ; set up the second-stage RWTS ; (the original disk did this at ; $0441 in the sector that it read ; at the last minute, but I can't ; jump to it directly because it's ; intertwingled with a call to the ; original RWTS on the text page, ; which doesn't exist) 089F- 8D E9 B7 STA $B7E9 08A2- 8D F7 B7 STA $B7F7 08A5- 4A LSR 08A6- 4A LSR 08A7- 4A LSR 08A8- 4A LSR 08A9- AA TAX ; tell second-stage RWTS that we're ; on track $13, so it doesn't grind ; the disk when it loads level data 08AA- A9 13 LDA #$13 08AC- 9D 78 04 STA $0478,X 08AF- 9D F8 04 STA $04F8,X ; jump to the game code (originally ; at $0450) to initialize zero page ; and start the game 08B2- 4C 50 90 JMP $9050 The entire boot process takes about two seconds -- a 5x speed increase from the original disk. Copy protection is expensive. Quod erat liberandum. ~ Acknowledgements Thanks to qkumba for writing 0boot, for explaining 6-and-2 encoding to me, for reviewing drafts of this write-up, and for being that rare combination of smart and kind. (Kids, don't put up with genius jerks. There are genius non-jerks. Find them. Nurture them. Cherish them.) ~ Changelog 2023-08-01 - updated description of fluttering (it isn't quarter tracks after all) [thanks Kent D.] 2020-06-24 - typo in the 6-and-2 encoding diagram [thanks Andrew R.] 2018-10-20 - initial release --------------------------------------- A 4am crack No. 1899 ------------------EOF------------------