Microleague Baseball Box Score/Stat Compiler Crack Info Hot Rod October 2012 Intro So my retro computing project for the last few weeks (off and on) has been converting the Micro League Baseball Box Score/Stat Compiler disk to a ‘normal’ 16-sector format (aka ‘cracking’ it). After I’d imaged my disks and uploaded new additions to Asimov, I learned that I had the Micro League Baseball game disk, the General Manager/Owner disk, and Team disks for 1983 and 1984. Magnusfalkirk posted that he also had this Box Score/Stat Compiler disk, and apparently it’s not been cracked. He had working .nib images of it, the GM disk, and the regular MLB game disk, so it seemed possible to work on the Box Score disk from the .nib image to crack it. Since they are .nib images though, they can’t be (easily) converted back to regular disks to run on a real Apple II. Way back when, I had cracked a beta of MLB, and vaguely recalled it. I do still have a hardcopy printout of its disk routines (but without many notes). I at least knew it was all 4+4 encoded, and a crack would require replacing the RWTS and wiring it in. I had done the first one remotely (never saw the real disk), and I remember it didn’t take all that long, but beyond that, I don’t remember what all I did. The other images I’d uploaded to Asimov were all cracks done by my buddy, The Cloak, and I figured I’d be better off using those for reference instead, since there were more of them, and they matched the .nib images. My general idea was to convert the 4+4 encoded, 10-sector disk to regular 6+2 encoded, 16-sector format, and use the ‘shell’ from The Cloak’s GM disk crack to adapt and ‘run’ it. At least that was the idea… So before paying much attention to other cracks, I set off to first understand how the MLB Box Score disk worked, and at least get it converted to a regular format. I kept a fair amount of notes during the process, so I’ll break this up into separate posts, due to the overall length. Think ‘epic’. :) The Boot (or, you had me at EOR…) The boot0 starts out like this: 0801- 86 20 STX $20 0803- A2 00 LDX #$00 0805- 5D 00 08 EOR $0800,X 0808- 9D 00 01 STA $0100,X 080B- E8 INX 080C- D0 F7 BNE $0805 080E- AA TAX 080F- 9A TXS 0810- 60 RTS $800 contains a #$04, so basically everything loads right away ($800-$BFF). This lovely little routine then decodes itself down onto the stack, sets the stack pointer, and then begins a series of returns to control program flow. Neat. Stack return addresses that control the rest of the boot flow start at $1ED, based on the final value left in the accumulator. (I saved the decoded version out at $1100 in my work disk image, so it’s moved up to $11ED in saved file.) Following the list of return addresses on the stack, the rest of the boot sequence then is: a) $1B4 - this decodes (EOR) the code at $800-$BFF b) $184 - checks if 64K memory or not (and fails if not, although this has a bug in it). If yes, leaves upper RAM open for loading. The bug is an incorrect “jump to self” address, which I went ahead and fixed in the crack. c) $178 - stores #$01 at $22 and #$00 at $24 and $801 (which initializes for next reads) d) $14E - this reads in a batch of five T/S list groups (see map later on) e) $A700 - Main program. Play a game, See or print stats, Delete a team from disk are the only options. If the ‘P’ option is chosen (to play a game), then the next stack return address is used to continue back from here. f) $141 - turns the drive back on, stores a #$0B at $016F, loads A-reg from $0801 and falls through to $014E. This will read the next batch of six T/S list groups, however this is AFTER prompting to insert the MLB game disk (and checking it). So this next batch of six is for the Game Disk. g) $A700 - Start of Game disk (play, demo, etc) h) $6000 - Start of game The last two steps are actually running the code from the Game disk itself, although these last few stack returns are from the original Box Score/Stat disk boot. An interesting crossover twist (and one that presented a slight challenge to the crack – more on that later). The Format and Rest of Boot Track $00 is a normal 16-sector format, but only has the few boot sectors’ worth of data on it. Tracks $01-$22 are all 4+4 encoded, each with ten sectors’ worth of data on them. The sectors themselves are interesting, in that they have an address field with a marker of ED AD F5, followed by the sector number, and then this is followed by a data field with marker that is also ED AD F5, but is followed by Track, Sector, Byte Count, and Checksum. However, the track and sector that are in the data field marker are for the NEXT track and sector to be read; it’s a linked list. If there are no more sectors to read, then the ‘next’ sector is set to zero. The byte count is the count of bytes to read for the current sector though. More on this a bit later. The sector number has the hi-bit set, so #$80-#$89 are sectors 0-9 (ten total, since 4+4 encoded). To make it easier to read the encoding, I jotted down a translate table. Sector number nibble encoding: EA AA = 80 EA AB = 81 EB AA = 82 EB AB = 83 EA AE = 84 EA AF = 85 EB AE = 86 EB AF = 87 EE AA = 88 EE AB = 89 So one thing I could see that I’d need is a routine that scans tracks $1-$22 and builds a map of the linked lists, to see what sectors are used. “Files” that are stored via this scheme are just spread out as linked sectors, but there is no catalog or VTOC; the knowledge of what’s where on the disk is custom coded in a table. Now, back to the boot. The routine at $178 initialized the following: $22 = drive number (1 or 2) $24 = current track $801 = count the number of T/S 'groups' to read And then the boot continued at $14E, which I’ve annotated below. 14E- 0A ASL ; shift left (x2) 14F- 0A ASL ; shift left (x2), so skip 4 150- A8 TAY ; Y now skipped four byte set 151- B9 11 08 LDA $0811,Y ; load track (x2 - half tracks) 154- 85 2C STA $2C ; store track 156- B9 12 08 LDA $0812,Y ; load sector (hi-bit set) 159- 85 2E STA $2E ; store sector 15B- B9 13 08 LDA $0813,Y ; load buffer address lo 15E- 85 34 STA $34 ; store buffer address lo 160- B9 14 08 LDA $0814,Y ; load buffer address hi 163- 85 35 STA $35 ; store buffer address hi 165- 20 00 09 JSR $0900 ; go read one group 168- EE 01 08 INC $0801 ; increment T/S group count 16B- AD 01 08 LDA $0801 ; load for compare 16E- C9 05 CMP #$05 ; compare count 170- 90 DC BCC $014E ; next group if not done 172- A6 20 LDX $20 ; load the disk slot# 174- BD 88 C0 LDA $C088,X ; turn off disk 177- 60 RTS ; return From this we see that the table for track, sector, and buffer addresses is at $811, stored as four-byte groups. The routine at $14E is loading the first five sets. Looking at the table, we find: Tr Sc AL AH 0811- 0C 86 00 04 ;$400 0815- 02 84 00 22 ;$2200 0819- 10 86 00 D4 ;$D400 081D- 18 86 00 60 ;$6000 0821- 26 87 00 A7 ;$A700 The track number is in half tracks, and the sector number has the hi-bit set. After decoding (and knowing where they end), these turn out to be: 0811: $0400-$07FF, start Track $06, Sector 6 to Track $06, sector 9 [title page text] 0815: $2200-$275D start Track $01, Sector 4 to Track $01, sector 9 [code - no disk routines, seems like text support] 0819: $D400-$E7DA start Track $08, Sector 6 to Track $0A, sector 5 [code - has two calls to $900] 081D: $6000-$A6C2 start Track $0C, Sector 6 to Track $13, sector 6 [code] 0821: $A700-$BEBE start Track $13, Sector 7 to Track $16, sector 0 [code/text - calls $900 and has disk write routines $B9E0-$BEBE] Which, as can be seen, don’t account for the entire disk, and which contain some interesting things. But after that last group is loaded, program execution goes to $A700, and the Box Score/Stat Compiler is up and running. The RWTS (well, RTS at least) Having gone through the basic boot, program load and start, and the disk format, I needed to understand the RWTS in order to use it for converting the sectors to regular format, but I now also knew I needed to use it to get a sector mapping of the disk; the load table only provided the starting sector and buffer, but not a length. So next I studied the RWTS. It wasn’t very large, and I soon realized it really only handled reading (so more accurately, it’s an RTS routine I suppose). I created an annotated listing of it, which I’ll just plop in here and leave it at that. 0900- 20 B0 0A JSR $0AB0 ;check if reached T/S limits 0903- B0 45 BCS $094A ;if either limit reached, then branch to err_display 0905- A4 22 LDY $22 ;drive num 0907- B9 23 00 LDA $0023,Y ;get track of drive 090A- 8D F8 0B STA $0BF8 ;store into current track 090D- A9 EF LDA #$EF ;odd - $B6A repeats this as first ops anyway 090F- 85 29 STA $29 0911- A9 D8 LDA #$D8 0913- 85 2A STA $2A 0915- 20 6A 0B JSR $0B6A ;seek to desired track 0918- A4 22 LDY $22 ;drive num 091A- A5 2C LDA $2C ;get desired track 091C- 99 23 00 STA $0023,Y ;store as current track (seek worked, so there now) 091F- A9 04 LDA #$04 ;retry count 0921- 8D BC 09 STA $09BC ;store retry count 0924- 20 BE 09 JSR $09BE ;read 0927- 90 08 BCC $0931 ;no err, continue 0929- CE BC 09 DEC $09BC ;decrement retry count 092C- D0 F6 BNE $0924 ;if not exhausted, retry 092E- 4C 4A 09 JMP $094A ;all retries exhausted, fail 0931- C6 3A DEC $3A ;how many bytes there were in sector to read 0933- 38 SEC 0934- A5 34 LDA $34 ;buff add_lo 0936- 65 3A ADC $3A ;add how many bytes read 0938- 85 34 STA $34 ;new buff add_lo 093A- 90 02 BCC $093E ;if didn't cross page, branch 093C- E6 35 INC $35 ;increment buff add_hi 093E- A5 30 LDA $30 ;next track (from nibble encoding) 0940- 85 2C STA $2C ;store track 0942- A5 32 LDA $32 ;next sector (from nibble encoding) 0944- 85 2E STA $2E ;store sector - if <#$80, then done 0946- 30 B8 BMI $0900 ;not done, continue 0948- 18 CLC ;clear carry, all good 0949- 60 RTS ;return 094A- A6 20 LDX $20 ;start of error message display routine 094C- BD 88 C0 LDA $C088,X ;turn off drive 094F- AD BD 09 LDA $09BD ;will be set to error code from failed routine (1, 2, 3 or 4) 0952- 20 8F 09 JSR $098F ;convert value in A-reg to two-char ASCII 0955- 8D B3 09 STA $09B3 ;save hi-nibb char 0958- 8E B4 09 STX $09B4 ;save lo-nibb char 095B- A5 2C LDA $2C ;load track num (in half tracks) 095D- 20 8F 09 JSR $098F ;convert to ASCII 0960- 8D B6 09 STA $09B6 ;save hi-nibb 0963- 8E B7 09 STX $09B7 ;save lo-nibb 0966- A5 2E LDA $2E ;load sector num 0968- 20 8F 09 JSR $098F ;convert to ASCII 096B- 8D B9 09 STA $09B9 ;save hi-nibb 096E- 8E BA 09 STX $09BA ;save lo-nibb 0971- A9 AA LDA #$AA 0973- 85 34 STA $34 ;store address_lo of error text 0975- A9 09 LDA #$09 0977- 85 35 STA $35 ;store address_hi of error text ($9AA) 0979- A0 11 LDY #$11 ;length of error text 097B- B1 34 LDA ($34),Y ;load char of error text 097D- 99 8A 04 STA $048A,Y ;display on text screen 0980- 88 DEY 0981- 10 F8 BPL $097B ;repeat until whole error text stored 0983- AD 54 C0 LDA $C054 ;display page1 0986- AD 51 C0 LDA $C051 ;display text 0989- 20 4B 0B JSR $0B4B ;beep routine (sounds just like the reset beep) 098C- 4C 8C 09 JMP $098C ;hang (end of error message display) 098F- 48 PHA ;routine to convert A-reg to two-char ASCII 0990- 29 0F AND #$0F 0992- 20 9F 09 JSR $099F 0995- AA TAX 0996- 68 PLA 0997- 4A LSR 0998- 4A LSR 0999- 4A LSR 099A- 4A LSR 099B- 20 9F 09 JSR $099F 099E- 60 RTS 099F- C9 0A CMP #$0A 09A1- B0 03 BCS $09A6 09A3- 69 B0 ADC #$B0 09A5- 60 RTS 09A6- 18 CLC 09A7- 69 B7 ADC #$B7 09A9- 60 RTS ;end of convert routine 09AA- A0 C5 LDY #$C5 ;start of error text 09AC- D2 ??? 09AD- D2 ??? 09AE- CF ??? 09AF- D2 ??? 09B0- A0 A3 LDY #$A3 09B2- A0 00 LDY #$00 09B4- 00 BRK 09B5- AD 00 00 LDA $0000 09B8- AD 00 00 LDA $0000 09BB- A0 ;end of error text 09BC- 00 ;retry count 09BD- 00 BRK ;error code 09BE- 20 F5 0A JSR $0AF5 ;read address field and desired sector 4+4 encoded (ED AD F5 xx xx) 09C1- 90 03 BCC $09C6 ;success, branch 09C3- 4C AE 0A JMP $0AAE ;jump to set carry and return 09C6- EA NOP 09C7- 4C CA 09 JMP $09CA 09CA- BD 8C C0 LDA $C08C,X ;now look for data field (ED AD F5 again) 09CD- 10 FB BPL $09CA 09CF- C9 ED CMP #$ED 09D1- D0 F3 BNE $09C6 ;interesting possible infinite loop here if markers not found... 09D3- EA NOP 09D4- B5 00 LDA $00,X 09D6- B5 00 LDA $00,X 09D8- BD 8C C0 LDA $C08C,X 09DB- 10 FB BPL $09D8 09DD- C9 AD CMP #$AD 09DF- D0 EE BNE $09CF 09E1- B5 00 LDA $00,X 09E3- B5 00 LDA $00,X 09E5- EA NOP 09E6- BD 8C C0 LDA $C08C,X 09E9- 10 FB BPL $09E6 09EB- C9 F5 CMP #$F5 09ED- D0 E0 BNE $09CF 09EF- EA NOP ;have data field markers, now read TRACK, SECTOR, BYTE COUNT, CHECKSUM 09F0- B5 00 LDA $00,X 09F2- B5 00 LDA $00,X 09F4- A9 00 LDA #$00 ;init checksum 09F6- 85 3C STA $3C ;store checksum var 09F8- BD 8C C0 LDA $C08C,X 09FB- 10 FB BPL $09F8 09FD- 2A ROL ;decode 4+4 hi 09FE- 85 3E STA $3E ;save in scratch 0A00- BD 8C C0 LDA $C08C,X 0A03- 10 FB BPL $0A00 0A05- 25 3E AND $3E ;decode 4+4 lo 0A07- 85 30 STA $30 ;store TRACK num (this should be "next" track to read) 0A09- 45 3C EOR $3C ;update checksum 0A0B- 85 3C STA $3C ;store checksum update 0A0D- EA NOP 0A0E- C5 3C CMP $3C 0A10- 38 SEC 0A11- BD 8C C0 LDA $C08C,X 0A14- 10 FB BPL $0A11 0A16- 2A ROL ;decode 4+4 hi 0A17- 85 3E STA $3E ;save in scratch 0A19- BD 8C C0 LDA $C08C,X 0A1C- 10 FB BPL $0A19 0A1E- 25 3E AND $3E ;decode 4+4 lo 0A20- 85 32 STA $32 ;store SECTOR num (this should be "next" sector to read, or 0 if no next) 0A22- EA NOP 0A23- 45 3C EOR $3C ;update checksum 0A25- 85 3C STA $3C ;store checksum update 0A27- C5 3C CMP $3C 0A29- 38 SEC 0A2A- BD 8C C0 LDA $C08C,X 0A2D- 10 FB BPL $0A2A 0A2F- 2A ROL ;decode 4+4 hi 0A30- 85 3E STA $3E ;save in scratch 0A32- BD 8C C0 LDA $C08C,X 0A35- 10 FB BPL $0A32 0A37- 25 3E AND $3E ;decode 4+4 lo 0A39- 85 3A STA $3A ;store BYTE COUNT 0A3B- EA NOP 0A3C- 45 3C EOR $3C ;update checksum 0A3E- 85 3C STA $3C ;store checksum update 0A40- C5 3C CMP $3C 0A42- 38 SEC 0A43- BD 8C C0 LDA $C08C,X 0A46- 10 FB BPL $0A43 0A48- 2A ROL ;decode 4+4 hi 0A49- 85 3E STA $3E ;save in scratch 0A4B- BD 8C C0 LDA $C08C,X 0A4E- 10 FB BPL $0A4B 0A50- 25 3E AND $3E ;decode 4+4 lo 0A52- 45 3C EOR $3C ;update checksum 0A54- F0 0A BEQ $0A60 ;if checksum is correct, then will be zero and branch 0A56- 85 3C STA $3C ;checksum is not correct 0A58- A9 03 LDA #$03 ;error code 3 0A5A- 8D BD 09 STA $09BD ;store error code 0A5D- 4C AE 0A JMP $0AAE ;jump to error exit 0A60- EA NOP 0A61- A0 00 LDY #$00 ;now are going to read the data field, decode 4+4 and store in buffer 0A63- 84 3C STY $3C ;init data checksum 0A65- C5 3C CMP $3C 0A67- BD 8C C0 LDA $C08C,X 0A6A- 10 FB BPL $0A67 0A6C- 38 SEC 0A6D- 2A ROL 0A6E- 85 3E STA $3E 0A70- 85 3E STA $3E 0A72- EA NOP 0A73- BD 8C C0 LDA $C08C,X 0A76- 10 FB BPL $0A73 0A78- 25 3E AND $3E ;byte is now 4+4 decoded 0A7A- 91 34 STA ($34),Y ;store it in buffer 0A7C- 45 3C EOR $3C ;update checksum 0A7E- 85 3C STA $3C ;store checksum update 0A80- C8 INY ;increment bytes read 0A81- C4 3A CPY $3A ;compare to number of bytes in sector to read 0A83- D0 E0 BNE $0A65 ;more bytes to read so keep going 0A85- EA NOP ;have read all the bytes, so get checksum 0A86- 38 SEC 0A87- BD 8C C0 LDA $C08C,X 0A8A- 10 FB BPL $0A87 0A8C- 2A ROL 0A8D- 85 3E STA $3E 0A8F- BD 8C C0 LDA $C08C,X 0A92- 10 FB BPL $0A8F 0A94- 25 3E AND $3E 0A96- 45 3C EOR $3C ;update checksum 0A98- 85 3C STA $3C ;store checksum update 0A9A- 98 TYA ;transfer count of bytes read 0A9B- D0 06 BNE $0AA3 ;if not 256 bytes, then branch 0A9D- A5 3C LDA $3C ;load checksum 0A9F- D0 06 BNE $0AA7 ;should be zero if read 256 bytes 0AA1- 18 CLC ;checksum good, indicate success 0AA2- 60 RTS ;return (end of read data) 0AA3- C8 INY ;need to pad reads to 256 0AA4- 4C 87 0A JMP $0A87 ;loop until read 256 bytes, but don't store these in read buffer 0AA7- 85 3C STA $3C ;store checksum (shouldn't be needed; just read it from here) 0AA9- A9 04 LDA #$04 ;error code 4 0AAB- 8D BD 09 STA $09BD ;store error code 0AAE- 38 SEC ;indicate error 0AAF- 60 RTS ;return 0AB0- 38 SEC ;start routine to check sector and track limits 0AB1- A5 2E LDA $2E ;load sector number ($80-$89, hi-bit is set) 0AB3- 10 10 BPL $0AC5 ;err if hi-bit not set 0AB5- C9 8A CMP #$8A ; 0AB7- B0 0C BCS $0AC5 ;err if beyond tenth sector (only have ten with 4+4 encoding) 0AB9- 85 32 STA $32 ;store sector num 0ABB- A5 2C LDA $2C ;load track number (half tracks) 0ABD- C9 46 CMP #$46 ;compare to max half tracks 0ABF- B0 04 BCS $0AC5 ;err if beyond max half tracks ($22 tracks, $44 half tracks 0AC1- 85 30 STA $30 ;store half track num 0AC3- 18 CLC ;no err, so clear carry 0AC4- 60 RTS ;end of limit check 0AC5- A9 01 LDA #$01 ;err #1 indicates this routine check 0AC7- 8D BD 09 STA $09BD ;store in error code 0ACA- 60 RTS 0ACB- A6 20 LDX $20 ;begin enable drive read routine (get slot num) 0ACD- BD 8A C0 LDA $C08A,X ;engage drive 1 0AD0- A5 22 LDA $22 ;drive number 0AD2- C9 01 CMP #$01 ;drive 1? 0AD4- F0 03 BEQ $0AD9 ;if yes, skip drive 2 0AD6- BD 8B C0 LDA $C08B,X ;engage drive 2 0AD9- BD 89 C0 LDA $C089,X ;turn on drive 0ADC- BD 8E C0 LDA $C08E,X ;prepare latch for input 0ADF- A9 C8 LDA #$C8 ;spin-up delay 0AE1- 20 E5 0A JSR $0AE5 ;delay 0AE4- 60 RTS ;return (end enable drive read routine) 0AE5- A0 00 LDY #$00 ;begin delay 0AE7- 38 SEC 0AE8- D0 00 BNE $0AEA 0AEA- 20 F4 0A JSR $0AF4 0AED- 88 DEY 0AEE- D0 F8 BNE $0AE8 0AF0- E9 01 SBC #$01 0AF2- D0 F4 BNE $0AE8 0AF4- 60 RTS ;end of delay 0AF5- A6 20 LDX $20 ;begin read address field (get slot num) 0AF7- A0 00 LDY #$00 0AF9- 84 3C STY $3C 0AFB- C8 INY 0AFC- D0 04 BNE $0B02 0AFE- E6 3C INC $3C 0B00- F0 42 BEQ $0B44 ;pretty generous retry count- 255*255 0B02- BD 8C C0 LDA $C08C,X ;looking for address marker of ED AD F5 0B05- 10 FB BPL $0B02 ;otherwise fairly staightforward 0B07- C9 ED CMP #$ED 0B09- D0 F0 BNE $0AFB 0B0B- EA NOP 0B0C- B5 00 LDA $00,X 0B0E- B5 00 LDA $00,X 0B10- BD 8C C0 LDA $C08C,X 0B13- 10 FB BPL $0B10 0B15- C9 AD CMP #$AD 0B17- D0 EE BNE $0B07 0B19- B5 00 LDA $00,X 0B1B- B5 00 LDA $00,X 0B1D- A0 03 LDY #$03 ;while it loads Y like it will be reading a Vol/T/S/Chksum, it only looks for sector 0B1F- BD 8C C0 LDA $C08C,X 0B22- 10 FB BPL $0B1F 0B24- C9 F5 CMP #$F5 0B26- D0 DF BNE $0B07 0B28- EA NOP 0B29- 85 3E STA $3E 0B2B- B5 00 LDA $00,X 0B2D- B5 00 LDA $00,X 0B2F- BD 8C C0 LDA $C08C,X 0B32- 10 FB BPL $0B2F 0B34- 2A ROL ;rotate nibble left (first part of 4+4 decode) 0B35- 85 3E STA $3E ;and store where building byte 0B37- BD 8C C0 LDA $C08C,X 0B3A- 10 FB BPL $0B37 0B3C- 25 3E AND $3E ;build 8-bit byte (second part of 4+4 decode) 0B3E- C5 2E CMP $2E ;compare to desired sector 0B40- D0 B9 BNE $0AFB ;get the right one? Branch back if no 0B42- 18 CLC ;matches, indicate no error 0B43- 60 RTS ;return (end read address field) 0B44- A9 02 LDA #$02 ;error code 2 0B46- 8D BD 09 STA $09BD ;store error code 0B49- 38 SEC ;indicate error 0B4A- 60 RTS ;return (end read address field - error) 0B4B- A9 40 LDA #$40 ;beep routine (sounds like the reset beep) 0B4D- 20 5E 0B JSR $0B5E 0B50- A0 C0 LDY #$C0 0B52- A9 0C LDA #$0C 0B54- 20 5E 0B JSR $0B5E 0B57- AD 30 C0 LDA $C030 0B5A- 88 DEY 0B5B- D0 F5 BNE $0B52 0B5D- 60 RTS ;end beep routine 0B5E- 38 SEC ;delay routine 0B5F- 48 PHA 0B60- E9 01 SBC #$01 0B62- D0 FC BNE $0B60 0B64- 68 PLA 0B65- E9 01 SBC #$01 0B67- D0 F6 BNE $0B5F 0B69- 60 RTS ;end delay routine 0B6A- A9 EF LDA #$EF ;seekabs routine 0B6C- 85 29 STA $29 0B6E- A9 D8 LDA #$D8 0B70- 85 2A STA $2A 0B72- A6 20 LDX $20 ;slot 0B74- A5 2C LDA $2C ;desired track 0B76- CD F8 0B CMP $0BF8 ;current track 0B79- F0 53 BEQ $0BCE ;already there, exit 0B7B- A9 00 LDA #$00 0B7D- 85 3E STA $3E 0B7F- AD F8 0B LDA $0BF8 0B82- 85 27 STA $27 ;$27 will be prior track 0B84- 38 SEC 0B85- E5 2C SBC $2C ;$2C will be final (as will $BF8) 0B87- F0 33 BEQ $0BBC 0B89- B0 07 BCS $0B92 0B8B- 49 FF EOR #$FF 0B8D- EE F8 0B INC $0BF8 0B90- 90 05 BCC $0B97 0B92- 69 FE ADC #$FE 0B94- CE F8 0B DEC $0BF8 0B97- C5 3E CMP $3E 0B99- 90 02 BCC $0B9D 0B9B- A5 3E LDA $3E 0B9D- C9 0C CMP #$0C 0B9F- B0 01 BCS $0BA2 0BA1- A8 TAY 0BA2- 38 SEC 0BA3- 20 C0 0B JSR $0BC0 0BA6- B9 E0 0B LDA $0BE0,Y 0BA9- 20 CF 0B JSR $0BCF 0BAC- A5 27 LDA $27 0BAE- 18 CLC 0BAF- 20 C3 0B JSR $0BC3 0BB2- B9 EC 0B LDA $0BEC,Y 0BB5- 20 CF 0B JSR $0BCF 0BB8- E6 3E INC $3E 0BBA- D0 C3 BNE $0B7F 0BBC- 20 CF 0B JSR $0BCF 0BBF- 18 CLC 0BC0- AD F8 0B LDA $0BF8 0BC3- 29 03 AND #$03 0BC5- 2A ROL 0BC6- 05 20 ORA $20 0BC8- AA TAX 0BC9- BD 80 C0 LDA $C080,X 0BCC- A6 20 LDX $20 0BCE- 60 RTS ;end seekabs ($BF8 is final track) 0BCF- A2 11 LDX #$11 ;arm move delay subroutine 0BD1- CA DEX 0BD2- D0 FD BNE $0BD1 0BD4- E6 29 INC $29 0BD6- D0 02 BNE $0BDA 0BD8- E6 2A INC $2A 0BDA- 38 SEC 0BDB- E9 01 SBC #$01 0BDD- D0 F0 BNE $0BCF 0BDF- 60 RTS ;end arm move delay 0BE0- 01 30 ORA ($30,X) ;arm move delay table start 0BE2- 28 PLP 0BE3- 24 20 BIT $20 0BE5- 1E 1D 1C ASL $1C1D,X 0BE8- 1C ??? 0BE9- 1C ??? 0BEA- 1C ??? 0BEB- 1C ??? 0BEC- 70 2C BVS $0C1A 0BEE- 26 22 ROL $22 0BF0- 1F ??? 0BF1- 1E 1D 1C ASL $1C1D,X 0BF4- 1C ??? 0BF5- 1C ??? 0BF6- 1C ??? 0BF7- 1C ??? ;arm move delay table end 0BF8- 0C ??? ;current track 0BF9- F0 05 BEQ $0C00 ;unused (to $BFF)? 0BFB- BA TSX 0BFC- 22 ??? 0BFD- 54 0BFE- 48 0BFF- 45 Get the Map! From studying the RWTS routine, I knew I could use it to scan the disk and generate a map of the sector linked lists, as well as use it for converting the data. The first thing I wanted to do was generate the map though. While it would be better to dust off Merlin and create some tools in assembly, I’ll admit that I’ve not yet resurrected that skill set (hey, it’s on the list!). And these seemed like such small tasks, that it seemed like it would just be faster to write them directly as I went. So instead I just sketched pseudo-code outlines, and wrote them in machine language (yes, machine language – not even the built in mini-assembler). It builds character. Anyway, on my work disk are a couple raw routines to accomplish what I needed to do, but not what I’d point to as the best way to go. Even in machine language though, these didn’t take very long to whip out (for some reason, I seem to recall 6502 op codes and can write strings of this stuff out). That aside, here’s what I planned for getting the sector map: zpage stuff: $05: map_lo $06: map_hi $20: slot num $22: drive num $24: current track $2C: desired track $2E: desired sector $34: buff_lo $35: buff_hi want to grab: $30: next track $32: next sector $3A: byte count Routines: $ACB: enable drive for read $B6A: seekabs $9BE: read sector $AB0: init next track and sector Plan of Attack: Init Init slot num Init drive num Init current track to 0 (be sure to locate arm to track 0!) Init track num to 2 (half tracks - is track 1) Init sector num to 80 (sector 0) Init buff_lo (use $C00) Init buff_hi Init map_lo (use $2000) Init map_hi Enable drive for read Gather map For disk Seek track For each track Read sector store next track at map+0 (use $2000 as base) store next sector at map+1 store byte count at map+2 increment sector if sector <#$8A, repeat increment track (X2 for half track) if track <#$46, repeat Turn off drive exit Then I coded it up in machine language and ran it. It produced output like this: 2000- 02 00 FF 00 02 00 FF 00 2008- 02 00 FF 00 02 00 FF 00 2010- 02 85 00 00 02 86 00 00 2018- 02 87 00 00 02 88 00 00 2020- 02 89 00 00 04 00 5E 00 … [etc] Which I then cleaned up to be like: 02 00 FF 02 00 FF 02 00 FF 02 00 FF 02 85 00 02 86 00 02 87 00 02 88 00 02 89 00 04 00 5E And then that I tossed into a word processor and converted into a full blown table. This table I then converted into regular track sector notation, so for each track and sector, the ‘next’ track and sector is listed, the byte count, and then a comment field as to what the sector contained. This now gave me a complete view of what was on the disk, and what it was. And this was how I figured out that the track sector load table contained track/sector lists that didn’t match the Box Score disk. And that the Box Score disk contains stuff from the MLB Game disk (but is never accessed). And where all the teams are, etc. Good stuff. The Conversion Getting the sector map was a bit of a warm up to converting the disk. It allowed me to try out the native RWTS and make sure I knew how to run it. The next step was to convert all the 4+4 encoded data sectors into 6+2 encoded sectors and get out to regular disk. Normally this would be point of using Advanced Demuffin. This is exactly what it’s made to do. The only downside is that in some cases, you have to create a special IOB that avoids colliding with memory Advanced Demuffin uses, which is rare. But, this was one of those rare cases. MLB’s RWTS uses both zero page addresses, as well as sits in the same memory space as Advanced Demuffin. So, I had to decide if I wanted to create a special ‘adapter’ IOB that swapped stuff in and out (plus tweak Adv. Dem. to use ten sectors), or use something else. After weighing the amount of time/work involved with either approach, I opted to not use Advanced Demuffin, but instead just do it semi-manually. Since I already had code to ‘drive’ the RWTS through a sequence of track sectors, all I needed to do was keep the data buffered, and then write it back out to a regular disk. So I chose to modify my first routine, and pair it up with The Inspector for writing the data out to regular disk. I sketched out the plan as follows: zpage stuff: $20: slot num $22: drive num $24: current track $2C: desired track $2E: desired sector $34: buff_lo $35: buff_hi Routines: $ACB: enable drive for read $B6A: seekabs $9BE: read sector $AB0: init next track and sector Plan of Attack: Init Init slot num Init drive num Init current track to 0 (be sure to locate arm to track 0!) Init track num to 1, 9, 17, 25, 33 ($2, $12, $22, $32, $42 for half tracks) Init sector num to 80 (sector 0) Init buff_lo Init buff_hi (start at $1000) Enable drive for read Read track data into $1000-$19FF through $8000-$89FF (so eight tracks) For disk Seek track For each track Read sector increment buff_hi increment sector if sector <#$8A, repeat reset sector to #$80 advance buff_hi +6 (to take to next page) increment track (X2 for half tracks) if track <#$end_track, repeat ;end_track is next start range Turn off drive exit Use The Inspector to write buffer of sectors out to 16-sector disk Yes, this is pretty crude. Yes, it could be made much better. But it worked, and the disk data got converted. At this point I figured great – I’ve got the data, it’s intact as laid out, now I can just ‘wrap’ the engine from the other Black Bag cracks around this and be done. Ah, not so fast… Enter – Black Bag I suppose this would be a great spot to include some backstory on Black Bag and the like, but I’m not. Maybe another time – this is already way too long. :) Now, for some reason I had assumed that any normal crack would have kept the stuff the way it was, and just spliced in a 16-sector RWTS with as minimal intrusion as possible (far less work that way). But, I didn’t take into account that the cracks from which I wanted to borrow were done back in the 80’s, by The Cloak/Black Bag, when disks cost money, space was at a premium, and there was a certain point to be made about making things as space efficient and usable as possible. Several BB cracks from the period allow additional files to be stored on freed up disk space, co-resident with cracked programs needing full disk copiers to be used. And the series of MLB cracks were no exception. Ugh. So at this point I had to make a decision. I could either forge ahead and make the Box Score crack compatible with the other Black Bag cracks, create a singular crack of the Box Score disk with minimal change/effort, or re-crack the rest of the series to build a new set compatible with a minimal change/effort crack of the Box Score disk. Double ugh. Creating a singular crack of the Box Score disk made no sense, because it wouldn’t be able to work with any of the other disks, and the Play game option wouldn’t be able to work either. I considered re-cracking the whole series (game disk, GM disk, Team disks), but I didn’t have .nib images of the Team disks to work from, and to do the whole series I really would have to write a re-usable conversion tool instead of the one-time manual conversion I did. And since I’d been in Black Bag, it seemed kind of weird that things had come full circle back to this point, so I thought maybe I’d better round out the MLB set with one more BB crack. And that’s the path I picked. The only problem that presented was now I had to reverse engineer The Cloak’s cracks, not only to lift the RWTS and loader, but to make the Box Score disk compatible with the others (since the other disks were already compatible among themselves). The Cloak of course had taken advantage of having 16-sector tracks, and so had done some fairly significant re-arranging of the data layout as a result. And had re-wired the original program code to make use of the replacement RWTS. So I had to sort all that out and adapt the Box Score disk to it. But, not horribly complicated, and after studying it a bit, I could see the way forward. Here’s an overview of/notes on The Cloak’s take on the MLB cracks (based on the GM disk; game disk is similar). For boot: T0S0 loads to $800 T0S5 loads to $900 [new RTS] T0S6 loads to $A00 [new RTS] T0S7 loads to $B00 (copies to $300) T0S8 loads to $C00 (next four sectors are the text title page and get copied to $400-$7FF) T0S9 loads to $D00 T0sA loads to $E00 T0SB loads to $F00 T0SC loads to $1000 [these are the disk write routines] T0SD loads to $1100 T0SE loads to $1200 T0SF is empty (BB) $801 boot0 loads track $00, sectors $05 to $0E into $900 through $12FF (descending). Uses the ROM routine to do this. $860 copies $B00-$FFF to $300-$7FF then copies $1000-$12FF to $2100-$23FF [this is where the disk write routines land for the GM disk] then continues at $A50 $A50 sets up a 6+2 translate table at $856 (routine copied from ROM routine) $A6F calls $913 with index 0, then calls with index 1, then jumps to program. $A48 contains the track/sector/buffer load table $AD1 has routine to spread "Black Bag" on text screen and then reboot. $AF0 has routine to store #$BB at $3B00-$3BDA. This is called from within program as a replacement routine to clear buffer. $913 takes A-reg as input, x4, then loads track into $2C, sector into $2E, target buffer_hi into $34 and sector/page count into $35. Uses descending buffer addresses. Assumes 16-sector format. $974 had call to ROM WAIT routine at $FCA8, but since Box Score needs to load into AUX RAM, need ROM switched out. Copied ROM code to $AF0 and redirected call instead (Box Score only). Routine at $A2E in BB RWTS has a check for value #$BB that will skip copying into target buffer. The intent of this is to skip #$BB pad bytes at end of sectors. [What was really spooky about finding this is that before discovering this, when I had converted the Box Score disk, I initialized the buffer memory to #$BB. This resulted in the new 16-sector disk having #$BB as pad bytes already. It was our custom to use #$BB a lot, and I guess that old habit stuck. But wow.] Teams roster is on Track $1E, sectors 0 and 1 Team Reserve List pointer is on Track $1E, sector 2; this points to track $00, sector $04; list is track $00, sectors $01-$04 Track $1E, sector $03 to track $22, sector $0F is empty. Teams are on Track $16, sector $00 through Track $1D, sector $0F. Teams occupy four sectors each, 32 teams total. Armed with this info, I now could start the process of converting the conversion to “BB style”. Putting the Puzzle Together So at this point I had 6+2 encoded, ten-sectors per track data, and I wanted to map onto the Black Bag crack layout. I needed the Teams rosters and data to be in the same place as the other disks, so they’d all work together. That’s what I built next; I took just that data from the Box Score disk and laid it out on another disk, where the BB cracks expected it. That worked – the game disk and GM disk could find and read the teams from the Box Score disk. Then I lifted the boot from the GM crack and moved that over, but then modified to load the track/sector groups as per the original Box Score disk. The adaptation to the replacement RWTS is pretty simple; the original would set track into $2C, sector into $2E, buff_lo into $34 and buff_hi into $35, and then call $900. The Cloak tweaked it by instead setting the buff_hi into $34, and the count of sectors to read into $35, with an assumption that everything was descending (so the track/sector/buff_hi are all the last ones). Loads faster this way, but made the sector conversions more work. But, can still use a load table that has four-byte groups, and the RWTS calls from within the program itself can be modified in place, using the same zero page addresses. And all the calls are to $900, so just searching and updating those covered it. Beyond the 4+4 encoding and disk layout, the MLB programs have no secondary protection; the only protection is the format, so converting it is the entire effort. So the original map I had of the ten-sector tracks ended up with the data laid out quite differently on the 16-sector tracks: 0811: $0400-$07FF start Track $06, Sector 6 to Track $06, sector 9 [now t0s8-B] 0815: $2200-$275D start Track $01, Sector 4 to Track $01, sector 9 [left same] 0819: $D400-$E7DA start Track $08, Sector 6 to Track $0A, sector 5 [now t8sC thru t9sF] 081D: $6000-$A6C2 start Track $0C, Sector 6 to Track $13, sector 6 [now tCs9 to t10sF] 0821: $A700-$BEBE start Track $13, Sector 7 to Track $16, sector 0 [now t12s0 to t13s7] That got the data put in the right places, and I could now boot the game in ‘cracked’ form for the first time. The ‘S’ option worked, so I could see the stats, and after finding a couple more disk calls in the Aux RAM code, Printing stats worked too. That left the Play game option, and the Delete team options. Since I knew the path of the Play game option, and it was still just reads, I tackled that next. The Delete option was going to need disk write routines, so I saved that for last. Play the Game On the original Box Score disk, if the P option is selected, the user is prompted to switch to the game disk. And a check is made to ensure it is the game disk. If the check passes, then the next return address off the stack causes the program to continue by loading sector groups off the game disk. Why they didn’t just reboot the game disk is a mystery, but so be it; now I had a goal of preserving this approach, but intermixing the cracks of Box Score and the MLB game disk. The original table for loading the next sector groups from the game disk was: 0825- 04 80 00 BE ;$BE00 0829- 10 84 70 02 ;$270 082D- 10 86 00 0C ;$C00 0831- 14 88 00 28 ;$2800 0835- 18 84 00 40 ;$4000 0839- 1E 86 00 A7 ;$A700 083D- 04 83 00 A7 ;$A700 Which translates to: 0825: $BE00-$BFFF start Track $02, sector 0 to Track $02, sector 1 [teams roster - common to all disks] 0829: $0270-$036F start Track $08, sector 4 to [TBD] 082D: $0C00-$1FDA start Track $08, sector 6 to [TBD] 0831: $2800-$xxxx start Track $0A, sector 8 to [TBD] 0835: $4000-$xxxx start Track $0C, sector 4 to [TBD] 0839: $A700-$xxxx start Track $0F, sector 6 to [TBD] 083D: $A700-$xxxx start Track $02, sector 3 to Track $04, sector 7 [is game text, and gets loaded by game before start] Since I wasn’t going to be using these, and they really would require decoding the game disk, I didn’t chase them down. These were already covered by the MLB crack, and its load routine pulls in the equivalent to these in the replacement load table. My initial plan was to just patch in the load table to pick up the needed sectors, and let program control flow just like it did in the original. But I found that on the MLB game disk crack, some of the data to load had been blended with the crack boot and intro screen, so it wasn’t going to be a simple matter of just pointing to the data sectors and loading them; I had to leverage some of the game disk crack boot as well. So as a compromise, I just swapped in the code at $A00 from the MLB crack boot, and let it continue by itself. It doesn’t reboot, and just overlays a new sector load table and code to continue. That’s why you’ll find some unusual hopping about if you study that passage of the code. Another issue was that the MLB crack screen and original game screen had both been compressed, and intermixed into one routine. I’d have liked to separate them, and skip the crack screen, but that would have required some significant reworking. I was tight on space in that area, and just couldn’t get motivated enough to bother with it. These are already required to work as a ‘crack’ set, so I figured it was a small compromise to leave it in. Rebooting still would have been easier, but I at least avoided that, and the handoff from the Box Score disk to the game disk flows somewhat like the original at least. So at this point, I could boot, and the S and P options both seemed to work. That just left Delete, which required another dive into disk routines. Yer Outta Here – or Delete Teams As can be seen from the earlier look at the RWTS, there are no write routines in the RWTS residing at $900-$BFF. Yet the Box Score program allows team deletion as an option. And so does the General Manager/Owner program. So I knew they were around somewhere. I already had a clue where to look though, as I had seen disk routines in the $B900-$BF00 range from converting the disk. My thinking was that since the GM disk appeared to have the exact same function, I ought to be able to wire in the same write routines from it the same way. But there aren’t any write routines in the replacement RWTS in $900-$AFF either. It turns out that for both the MLB programs and the cracks, that there is a separate set of routines used only for writing. Between the GM program and the Box Score program, they are pretty much the same, but located in memory differently. As a result, the cracked version of the GM disk had them located where the GM program had them. I needed to relocate them to where the Box Score program had its original write routines instead. So rather than deeply studying the original write routines, I focused instead on lifting the ones from the GM disk crack and wiring them into the Box Score crack. But the original routines are fairly straightforward, and are pretty much spaced apart on page boundaries in memory, making it easier to decipher. For the Box Score program, the “write out new teams roster” starts at $B020. The track/sector is stored in $2C/$2E, starting ($34/$35) and ending ($37/$38) buffer addresses (lo/hi), and then the write routine entry is called, $B068. This then falls through to $B04E to write again. The actual entry to write buffers is at $B9E0. It is only called from $B068, so the 'D'elete routine is the only use. If you look at the code from $B9E0 to $BEBE, you’ll see that it contains the routines to write out the 4+4 encoded sectors, given a starting track/sector (at $2C/$2E), starting and ending buffers. The RWTS write routine for MLB GM crack has its entry at $21EB, but uses $2000-$23FF, although code is only within $2100-23FF. So the replacement routine was smaller than the original code footprint, making it easy to patch in. All I had to do was to relocate the addressing, and layer it over the sectors loaded by what the Box Score was already loading there: $2000 -> $BA00 $2100 -> $BB00 (t13, s4) $2200 -> $BC00 (t13, s5) $2300 -> $BD00 (t13, s6) Then I needed to match the modifications made to use the replacement write routines. Instead of passing in track/sector/start/end, it passes in just an index to lookup the info from another table. Since the delete is only limited to teams, and they are on fixed locations on all the disks, there are only two entries, so only two possible values are used. The only change to the table for use on the Box Score disk was a buffer address (which was different than the one used by the GM program). The resulting quick notes are: Patch calls at $B020+, which is on t12,s9 store #$02 at $37 and #$00 at $38 for first call (buffer was $3C00-$3DFF) store #$02 at $37 and #$01 at $38 for second call (buffer was $3800-$3BD9) NOP the rest of the setup code. With all this in place, now the Delete function works. Batting Clean Up Some final touches were needed to wrap things up. The other Black Bag MLB cracks all have a catalog track, and a VTOC that allow free space for files. I put a catalog track in place, but I opted to mark all the sectors as used. I didn’t think the need for squeezing free space out of disks was quite what it once was, and mixing other files onto full disk copy programs didn’t seem wise. Besides, the point of having been able to accomplish that with the cracks had already been made. What will be posted to Asimov will be the final MLB Box Score/Stat disk crack, .nib images for the other programs, a ‘scratch’ disk that contains portions of the program and the crude tools I used to extract the data, and a raw 16-sector image of the converted 10-sector data. Of course, this isn’t the only way to crack the MLB stuff. I think a more straightforward way would have been to just leave everything in its original 10-sector layout, and patch in the new RWTS code, leaving as much of the original program intact as possible. But as I mentioned earlier, it would require creating a whole new crack ‘set’ to allow them to work with each other. Another interesting alternative would be to create a 4+4 copy utility. Since the original code contains both the read and write routines, and they’re fairly straightforward to use, one could create a set of tools to create and/or copy the native 4+4 encoded disks. That might be an interesting project. (Or just extract the 4+4 disk routines and build a whole DOS around it.) The actual programs aren’t all that large though, so yet another alternative would be to just create file-based versions. Most of the direct disk access concerns the teams, and those all have a fixed structure to them. I think one could convert the whole series (Game, GM/Owner, Box Score, Teams) to a blended set of programs to play the game and manage the teams (i.e., the Box Score disk only adds the See/Print stats function; Play and Delete already existed). If not that, then the Game, GM, Box Score programs probably could fit all on one disk, with all the teams on other disks. The ’83 Teams disk I previously uploaded had a lost track $19, sector $0A, which I now know landed right in the data for the ’83 Houston Astros. It could be reconstructed with some effort, or replaced with another team. I also noticed that there are more teams on the Box Score disk that don’t appear, because they’re marked with a ‘00’ (which is the ‘deleted’ state). They might be salvageable. To make other Teams disks work with these cracks, all that is needed is for the teams data to land on the same track/sectors (as listed previously). Thanks again to Magnusfalkirk for providing the .nib images, and sucking me into this project. Back in the day, this would have been a day or two effort. Now it takes a little longer… :) ]HR