.%000! ;Little Red Reader MS-DOS file copier program
.%000! ;written 92/10/03 by Craig Bruce for C= Hacking Net Magazine
.%000!
The code is written for the Buddy assembler and here are a couple setup
directives. Note that my comments come before the section of code.
.%001! .org $8000
.%002! .obj "@:lrr.bin"
.%003!
.%004! ;====jump table and parameters interface ====
.%005!
.%006! jmp initPackage
.%007! jmp loadDirectory
.%008! jmp copyFile
.%009!
.%010! .byte $cb,131 ;identification
.%011! .byte 0,0,0,0
.%012!
These variables are included in the package program space to minimize unwanted
interaction with other programs loaded at the same time, such as the RAMDOS
device driver.
.%013! errno .buf 1 ;(location pk+15)
.%014! sourceDevice .buf 1
.%015! sourceType .buf 1 ;$00=1571, $ff=1581
.%016! startCluster .buf 2
.%017! lenML .buf 2 ;length medium and low bytes
.%018!
.%019! ;====global declaraions====
.%020!
.%021! kernelListen = $ffb1
.%022! kernelSecond = $ff93
.%023! kernelUnlsn = $ffae
.%024! kernelAcptr = $ffa2
.%025! kernelCiout = $ffa8
.%026! kernelSpinp = $ff47
.%027! kernelChkout = $ffc9
.%028! kernelClrchn = $ffcc
.%029! kernelChrout = $ffd2
.%030!
.%031! st = $d0
.%032! ciaClock = $dd00
.%033! ciaFlags = $dc0d
.%034! ciaData = $dc0c
.%035!
These are the parameters and derived parameters from the boot sector. They
are kept in the program space to avoid interactions.
.%036! clusterBlockCount .buf 1 ;1 or 2
.%037! fatBlocks .buf 1 ;up to 3
.%038! rootDirBlocks .buf 1 ;up to 8
.%039! rootDirEntries .buf 1 ;up to 128
.%040! totalSectors .buf 2 ;up to 1440
.%041! firstFileBlock .buf 1
.%042! firstRootDirBlock .buf 1
.%043! fileClusterCount .buf 2
.%044!
The cylinder (track) and side that is currently stored in the trach cache.
.%045! bufCylinder .buf 1
.%046! bufSide .buf 1
.%047! formatParms .buf 6
.%048!
This package is split into a number of levels. This level interfaces with the
Kernal serial bus routines and the burst command protocol of the disk drives.
.%049! ;====hardware level====
.%050!
Connect to the MS-DOS device and send the "U0" burst command prefix and the
burst command byte.
.%051! sendU0 = * ;( .A=burstCommandCode ) : .CS=err
.%052! pha
.%053! lda #0
.%054! sta st
.%055! lda sourceDevice
.%056! jsr kernelListen
.%057! lda #$6f
.%058! jsr kernelSecond
.%059! lda #"u"
.%060! jsr kernelCiout
.%061! bit st
.%062! bmi sendU0Error
.%063! lda #"0"
.%064! jsr kernelCiout
.%065! pla
.%066! jsr kernelCiout
.%067! bit st
.%068! bmi sendU0Error
.%069! clc
.%070! rts
.%071!
.%072! sendU0Error = *
.%073! lda #5
.%074! sta errno
.%075! sec
.%076! rts
.%077!
Toggle the "Data Accepted / Ready For More" clock signal for the burst
transfer protocol.
.%078! toggleClock = *
.%079! lda ciaClock
.%080! eor #$10
.%081! sta ciaClock
.%082! rts
.%083!
Wait for a burst byte to arrive in the serial data register of CIA#1 from the
fast serial bus.
.%084! serialWait = *
.%085! lda #$08
.%086! - bit ciaFlags
.%087! beq -
.%088! rts
.%089!
Wait for and get a burst byte from the fast serial bus, and send the "Data
Accepted" signal.
.%090! getBurstByte = *
.%091! jsr serialWait
.%092! ldx ciaData
.%093! jsr toggleClock
.%094! txa
.%095! rts
.%096!
Send the burst commands to "log in" the MS-DOS disk and set the Read sector
interleave factor.
.%097! mountDisk = * ;() : .CS=err
.%098! lda #%00011010
.%099! jsr sendU0
.%100! bcc +
.%101! rts
.%102! + jsr kernelUnlsn
.%103! bit st
.%104! bmi sendU0Error
.%105! clc
.%106! jsr kernelSpinp
.%107! bit ciaFlags
.%108! jsr toggleClock
.%109! jsr getBurstByte
.%110! sta errno
.%111! and #$0f
.%112! cmp #2
.%113! bcs mountExit
Grab the throw-away parameters from the mount operation.
.%114! ldy #0
.%115! - jsr getBurstByte
.%116! sta formatParms,y
.%117! iny
.%118! cpy #6
.%119! bcc -
.%120! clc
Set the sector interleave to 1 for a 1581 or 4 for a 1571.
.%121! ;** set interleave
.%122! lda #%00001000
.%123! jsr sendU0
.%124! bcc +
.%125! rts
.%126! + lda #1 ;interleave of 1 for 1581
.%127! bit sourceType
.%128! bmi +
.%129! lda #4 ;interleave of 4 for 1571
.%130! + jsr kernelCiout
.%131! jsr kernelUnlsn
.%132! mountExit = *
.%133! rts
.%134!
Read all of the sectors of a given track into the track cache.
.%135! bufptr = 2
.%136! secnum = 4
.%137!
.%138! readTrack = * ;( .A=cylinder, .X=side ) : trackbuf, .CS=err
.%139! pha
.%140! txa
Get the side and put it into the command byte. Remember that we have to flip
the side bit for a 1581.
.%141! and #$01
.%142! asl
.%143! asl
.%144! asl
.%145! asl
.%146! bit sourceType
.%147! bpl +
.%148! eor #$10
.%149! + jsr sendU0
.%150! bcc +
.%151! rts
.%152! + pla ;cylinder number
.%153! jsr kernelCiout
.%154! lda #1 ;start sector number
.%155! jsr kernelCiout
.%156! lda #9 ;sector count
.%157! jsr kernelCiout
.%158! jsr kernelUnlsn
Prepare to receive the track data.
.%159! sei
.%160! clc
.%161! jsr kernelSpinp
.%162! bit ciaFlags
.%163! jsr toggleClock
.%164! lda #.%165! ldy #>trackbuf
.%166! sta bufptr
.%167! sty bufptr+1
Get the sector data for each of the 9 sectors of the track.
.%168! lda #0
.%169! sta secnum
.%170! - bit sourceType
.%171! bmi +
If we are dealing with a 1571, we have to set the buffer pointer for the next
sector, taking into account the soft interleave of 4.
.%172! jsr get1571BufPtr
.%173! + jsr readSector
.%174! bcs trackExit
.%175! inc secnum
.%176! lda secnum
.%177! cmp #9
.%178! bcc -
.%179! clc
.%180! trackExit = *
.%181! cli
.%182! rts
.%183!
Get the buffer pointer for the next 1571 sector.
.%184! get1571BufPtr = *
.%185! lda #.%186! sta bufptr
.%187! ldx secnum
.%188! clc
.%189! lda #>trackbuf
.%190! adc bufptr1571,x
.%191! sta bufptr+1
.%192! rts
.%193!
.%194! bufptr1571 = *
.%195! .byte 0,8,16,6,14,4,12,2,10
.%196!
Read an individual sector into memory at the specified address.
.%197! readSector = * ;( bufptr ) : .CS=err
Get and check the burst status byte for errors.
.%198! jsr getBurstByte
.%199! sta errno
.%200! and #$0f
.%201! cmp #2
.%202! bcc +
.%203! rts
.%204! + ldx #2
.%205! ldy #0
.%206!
Receive the 512 sector data bytes into memory.
.%207! readByte = *
.%208! lda #$08
.%209! - bit ciaFlags
.%210! beq -
.%211! lda ciaClock
.%212! eor #$10
.%213! sta ciaClock
.%214! lda ciaData
.%215! sta (bufptr),y
.%216! iny
.%217! bne readByte
.%218! inc bufptr+1
.%219! dex
.%220! bne readByte
.%221! rts
.%222!
This next level of routines deals with logical sectors and the track cache
rather than with hardware.
.%223! ;====logical sector level====
.%224!
Invalidate the track cache if the MS-DOS drive number is changed or if a new
disk is inserted. This routine has to establish a RAM configuration of $0E
since it will be called from RAM0. Configuration $0E gives RAM0 from $0000 to
$BFFF, Kernal ROM from $C000 to $FFFF, and the I/O space over the Kernal from
$D000 to $DFFF. This configuration is set by all application interface
subroutines.
.%225! initPackage = *
.%226! lda #$0e
.%227! sta $ff00
.%228! lda #$ff
.%229! sta bufCylinder
.%230! sta bufSide
.%231! clc
.%232! rts
.%233!
Locate a sector (block) in the track cache, or read the corresponding physical
track into the track cache if necessary. This routine accepts the cylinder,
side, and sector numbers of the block.
.%234! sectorSave = 5
.%235!
.%236! readBlock = * ;( .A=cylinder,.X=side,.Y=sector ) : .AY=blkPtr,.CS=err
Check if the correct track is in the track cache.
.%237! cmp bufCylinder
.%238! bne readBlockPhysical
.%239! cpx bufSide
.%240! bne readBlockPhysical
If so, then locate the sector's address and return that.
.%241! dey
.%242! tya
.%243! asl
.%244! clc
.%245! adc #>trackbuf
.%246! tay
.%247! lda #.%248! clc
.%249! rts
.%250!
Here, we have to read the physical track into the track cache. We save the
input parameters and call the hardware-level track-reading routine.
.%251! readBlockPhysical = *
.%252! sta bufCylinder
.%253! stx bufSide
.%254! sty sectorSave
.%255! jsr readTrack
Check for errors.
.%256! bcc readBlockPhysicalOk
.%257! lda errno
.%258! and #$0f
.%259! cmp #11 ;disk change
.%260! beq +
.%261! sec
.%262! rts
If the error that happened is a "Disk Change" error, then mount the disk and
try to read the physical track again.
.%263! + jsr mountDisk
.%264! lda bufCylinder
.%265! ldx bufSide
.%266! ldy sectorSave
.%267! bcc readBlockPhysical
.%268! rts
.%269!
Here, the physical track has been read into the track cache ok, so we recover
the original input parameters and try the top of the routine again.
.%270! readBlockPhysicalOk = *
.%271! lda bufCylinder
.%272! ldx bufSide
.%273! ldy sectorSave
.%274! jmp readBlock
.%275!
Divide the given number by 18. This is needed for the calculations to convert
a logical sector number to the corresponding physical cylinder, side, and
sector numbers that the lower-level routines require. The method of repeated
subtraction is used. This routine would probably work faster if we tried to
repeatedly subtract 360 (18*20) at the top, but I didn't bother.
.%276! divideBy18 = * ;( .AY=number ) : .A=quotient, .Y=remainder
.%277! ;** could repeatedly subtract 360 here
.%278! ldx #$ff
.%279! - inx
.%280! sec
.%281! sbc #18
.%282! bcs -
.%283! dey
.%284! bpl -
.%285! clc
.%286! adc #18
.%287! iny
.%288! tay
.%289! txa
.%290! rts
.%291!
Convert the given logical block number to the corresponding physical cylinder,
side, and sector numbers. This routine follows the formulae given earlier
with a few simplifying tricks.
.%292! convertLogicalBlockNum = * ;( .AY=blockNum ) : .A=cyl, .X=side, .Y=sec
.%293! jsr divideBy18
.%294! ldx #0
.%295! cpy #9
.%296! bcc +
.%297! pha
.%298! tya
.%299! sbc #9
.%300! tay
.%301! pla
.%302! ldx #1
.%303! + iny
.%304! rts
.%305!
Copy a sequential group of logical sectors into memory. This routine is used
by the directory loading routine to load the FAT and Root Directory, and is
used by the cluster reading routine to retrieve all of the blocks of a
cluster. After the given starting logical sector number is converted into its
physical cylinder, side, and sector equivalent, the physical values are
incremented to get the address of successive sectors of the group. This
avoids the overhead of the logical to physical conversion. Quite a number of
temporaries are needed.
.%306! destPtr = 6
.%307! curCylinder = 8
.%308! curSide = 9
.%309! curSector = 10
.%310! blockCountdown = 11
.%311! sourcePtr = 12
.%312!
.%313! copyBlocks = * ;( .AY=startBlock, .X=blockCount, ($6)=dest ) : .CS=err
.%314! stx blockCountdown
.%315! jsr convertLogicalBlockNum
.%316! sta curCylinder
.%317! stx curSide
.%318! sty curSector
.%319!
.%320! copyBlockLoop = *
.%321! lda curCylinder
.%322! ldx curSide
.%323! ldy curSector
.%324! jsr readBlock
.%325! bcc +
.%326! rts
.%327! + sta sourcePtr
.%328! sty sourcePtr+1
.%329! ldx #2
.%330! ldy #0
Here I unroll the copying loop a little bit to cut the overhead of the branch
instruction in half. (A cycle saved... you know).
.%331! - lda (sourcePtr),y
.%332! sta (destPtr),y
.%333! iny
.%334! lda (sourcePtr),y
.%335! sta (destPtr),y
.%336! iny
.%337! bne -
.%338! inc sourcePtr+1
.%339! inc destPtr+1
.%340! dex
.%341! bne -
Increment the cylinder, side, sector values.
.%342! inc curSector
.%343! lda curSector
.%344! cmp #10
.%345! bcc +
.%346! lda #1
.%347! sta curSector
.%348! inc curSide
.%349! lda curSide
.%350! cmp #2
.%351! bcc +
.%352! lda #0
.%353! sta curSide
.%354! inc curCylinder
.%355! + dec blockCountdown
.%356! bne copyBlockLoop
.%357! clc
.%358! rts
.%359!
Read a cluster into the Cluster Buffer, given the cluster number. The cluster
number is converted to a logical sector number and then the sector copying
routine is called. The formula given earlier is used for the conversion.
.%360! readCluster = * ;( .AY=clusterNumber ) : clusterBuf, .CS=err
.%361! ;** convert cluster number to logical block number
.%362! sec
.%363! sbc #2
.%364! bcs +
.%365! dey
.%366! + ldx clusterBlockCount
.%367! cpx #1
.%368! beq +
.%369! asl
.%370! sty 7
.%371! rol 7
.%372! ldy 7
.%373! + clc
.%374! adc firstFileBlock
.%375! bcc +
.%376! iny
.%377!
.%378! ;** read logical blocks comprising cluster
.%379! + ldx #.%380! stx 6
.%381! ldx #>clusterBuf
.%382! stx 7
.%383! ldx clusterBlockCount
.%384! jmp copyBlocks
.%385!
This next level of routines deal with the data structures of the MS-DOS disk
format.
.%386! ;====MS-DOS format level====
.%387!
.%388! bootBlock = 2
.%389!
Read the disk format parameters, directory, and FAT into memory.
.%390! loadDirectory = * ;( ) : .AY=dirbuf, .X=dirEntries, .CS=err
.%391! lda #$0e
.%392! sta $ff00
.%393!
Read the boot sector and extract the parameters.
.%394! ;** get parameters from boot sector
.%395! lda #0
.%396! ldy #0
.%397! jsr convertLogicalBlockNum
.%398! jsr readBlock
.%399! bcc +
.%400! rts
.%401! + sta bootBlock
.%402! sty bootBlock+1
.%403! ldy #13 ;get cluster size
.%404! lda (bootBlock),y
.%405! sta clusterBlockCount
.%406! cmp #3
.%407! bcc +
.%408!
If a disk parameter is found to exceed the limits of LRR, error code #60 is
returned.
.%409! invalidParms = *
.%410! lda #60
.%411! sta errno
.%412! sec
.%413! rts
.%414!
.%415! + ldy #16 ;check FAT replication count, must be 2
.%416! lda (bootBlock),y
.%417! cmp #2
.%418! bne invalidParms
.%419! ldy #22 ;get FAT size in sectors
.%420! lda (bootBlock),y
.%421! sta fatBlocks
.%422! cmp #4
.%423! bcs invalidParms
.%424! ldy #17 ;get directory size
.%425! lda (bootBlock),y
.%426! sta rootDirEntries
.%427! cmp #129
.%428! bcs invalidParms
.%429! lsr
.%430! lsr
.%431! lsr
.%432! lsr
.%433! sta rootDirBlocks
.%434! ldy #19 ;get total sector count
.%435! lda (bootBlock),y
.%436! sta totalSectors
.%437! iny
.%438! lda (bootBlock),y
.%439! sta totalSectors+1
.%440! ldy #24 ;check sectors per track, must be 9
.%441! lda (bootBlock),y
.%442! cmp #9
.%443! bne invalidParms
.%444! ldy #26
.%445! lda (bootBlock),y
.%446! cmp #2 ;check number of sides, must be 2
.%447! bne invalidParms
.%448! ldy #14 ;check number of boot sectors, must be 1
.%449! lda (bootBlock),y
.%450! cmp #1
.%451! bne invalidParms
.%452!
Calculate the derived parameters.
.%453! ;** get derived parameters
.%454! lda fatBlocks ;first root directory sector
.%455! asl
.%456! clc
.%457! adc #1
.%458! sta firstRootDirBlock
.%459! clc ;first file sector
.%460! adc rootDirBlocks
.%461! sta firstFileBlock
.%462! lda totalSectors ;number of file clusters
.%463! ldy totalSectors+1
.%464! sec
.%465! sbc firstFileBlock
.%466! bcs +
.%467! dey
.%468! + sta fileClusterCount
.%469! sty fileClusterCount+1
.%470! lda clusterBlockCount
.%471! cmp #2
.%472! bne +
.%473! lsr fileClusterCount+1
.%474! ror fileClusterCount
.%475!
Gee, I have more comments embedded in the code than I did last issue.
.%476! ;** load FAT
.%477! + lda #.%478! ldy #>fatbuf
.%479! sta 6
.%480! sty 7
.%481! lda #1
.%482! ldy #0
.%483! ldx fatBlocks
.%484! jsr copyBlocks
.%485! bcc +
.%486! rts
.%487!
.%488! ;** load actual directory
.%489! + lda #.%490! ldy #>dirbuf
.%491! sta 6
.%492! sty 7
.%493! lda firstRootDirBlock
.%494! ldy #0
.%495! ldx rootDirBlocks
.%496! jsr copyBlocks
.%497! bcc +
.%498! rts
.%499! + lda #.%500! ldy #>dirbuf
.%501! ldx rootDirEntries
.%502! clc
.%503! rts
.%504!
This routine locates the given FAT table entry number and returns the value
stored in it. Some work is needed to deal with the 12-bit compressed data
structure.
.%505! entryAddr = 2
.%506! entryWork = 4
.%507! entryBits = 5
.%508! entryData0 = 6
.%509! entryData1 = 7
.%510! entryData2 = 8
.%511!
.%512! getFatEntry = * ;( .AY=fatEntryNumber ) : .AY=fatEntryValue
.%513! sta entryBits
Divide the FAT entry number by two and multiply by three because two FAT
entries are stored in three bytes. Then add the FAT base address and we have
the address of the three bytes that contain the FAT entry we are interested
in. I retrieve the three bytes into zero-page memory for easy manipulation.
.%514! ;** divide by two
.%515! sty entryAddr+1
.%516! lsr entryAddr+1
.%517! ror
.%518!
.%519! ;** times three
.%520! sta entryWork
.%521! ldx entryAddr+1
.%522! asl
.%523! rol entryAddr+1
.%524! clc
.%525! adc entryWork
.%526! sta entryAddr
.%527! txa
.%528! adc entryAddr+1
.%529! sta entryAddr+1
.%530!
.%531! ;** add base, get data
.%532! clc
.%533! lda entryAddr
.%534! adc #.%535! sta entryAddr
.%536! lda entryAddr+1
.%537! adc #>fatbuf
.%538! sta entryAddr+1
.%539! ldy #2
.%540! - lda (entryAddr),y
.%541! sta entryData0,y
.%542! dey
.%543! bpl -
.%544! lda entryBits
.%545! and #1
.%546! bne +
.%547!
If the original given FAT entry number is even, then we want the first 12-bit
compressed field. The nybbles are extracted according to the diagram shown
earlier.
.%548! ;** case 1: first 12-bit cluster
.%549! lda entryData1
.%550! and #$0f
.%551! tay
.%552! lda entryData0
.%553! rts
.%554!
Otherwise, we want the second 12-bit field.
.%555! ;** case 2: second 12-bit cluster
.%556! + lda entryData1
.%557! ldx #4
.%558! - lsr entryData2
.%559! ror
.%560! dex
.%561! bne -
.%562! ldy entryData2
.%563! rts
.%564!
Finally, this is the file copying level. It deals with reading the clusters
of MS-DOS files and copying the data they contain to the already-open CBM
Kernal file, possibly with ASCII-to-PETSCII translation.
.%565! ;====file copy level====
.%566!
.%567! transMode = 14
.%568! lfn = 15
.%569! cbmDataPtr = $60
.%570! cbmDataLen = $62
.%571! cluster = $64
.%572!
Copy the given cluster to the CBM output file. This routine fetches the next
cluster of the file for the next time this routine is called, and if it hits
the NULL pointer of the last cluster of a file, it adjusts the number of valid
file data bytes the current cluster contains to FileLength % ClusterLength
(see note below).
.%573! copyFileCluster = * ;( cluster, lfn, transMode ) : .CS=err
Read the cluster and setup to copy the whole cluster to the CBM file.
.%574! lda cluster
.%575! ldy cluster+1
.%576! jsr readCluster
.%577! bcc +
.%578! rts
.%579! + lda #.%580! ldy #>clusterBuf
.%581! sta cbmDataPtr
.%582! sty cbmDataPtr+1
.%583! lda #0
.%584! sta cbmDataLen
.%585! lda clusterBlockCount
.%586! asl
.%587! sta cbmDataLen+1
.%588!
Fetch the next cluster number of the file, and adjust the cluster data length
for the last cluster of the file.
.%589! ;**get next cluster
.%590! lda cluster
.%591! ldy cluster+1
.%592! jsr getFatEntry
.%593! sta cluster
.%594! sty cluster+1
.%595! cmp #$ff
.%596! bne copyFileClusterData
.%597! cpy #$0f
.%598! bne copyFileClusterData
.%599! lda lenML
.%600! sta cbmDataLen
.%601! lda #$01
.%602! ldx clusterBlockCount
.%603! cpx #1
.%604! beq +
.%605! lda #$03
.%606! + and lenML+1
The following three lines were added in a last minute panic after realizing
that if FileLength % ClusterSize == 0, then the last cluster of the file
contains ClusterSize bytes, not zero bytes.
.%000! bne +
.%000! ldx lenML
.%000! beq copyFileClusterData
.%607! + sta cbmDataLen+1
.%608!
.%609! copyFileClusterData = *
.%610! jsr commieOut
.%611! rts
.%612!
Copy the file data in the MS-DOS cluster buffer to the CBM output file.
.%613! cbmDataLimit = $66
.%614!
.%615! commieOut = * ;( cbmDataPtr, cbmDataLen ) : .CS=err
If the the logical file number to copy to is 0 ("null device"), then don't
bother copying anything.
.%616! ldx lfn
.%617! bne +
.%618! clc
.%619! rts
Otherwise, prepare the logical file number for output.
.%620! + jsr kernelChkout
.%621! bcc commieOutMore
.%622! sta errno
.%623! rts
.%624!
.%625! commieOutMore = *
Process the cluster data in chunks of up to 255 bytes or the number of data
bytes remaining in the cluster.
.%626! lda #255
.%627! ldx cbmDataLen+1
.%628! bne +
.%629! lda cbmDataLen
.%630! + sta cbmDataLimit
.%631! ldy #0
.%632! - lda (cbmDataPtr),y
.%633! bit transMode
.%634! bpl +
If we have to translate the current ASCII character, look up the PETSCII value
in the translation table and output that value. If the translation table
entry value is $00, then don't output a character (filter out invalid
character codes).
.%635! tax
.%636! lda transBuf,x
.%637! beq commieNext
.%638! + jsr kernelChrout
.%639! commieNext = *
.%640! iny
.%641! cpy cbmDataLimit
.%642! bne -
.%643!
Increment the cluster buffer pointer and decrement the cluster buffer character
count according to the number of bytes just processed, and repeat the above if
more file data remains in the current cluster.
.%644! clc
.%645! lda cbmDataPtr
.%646! adc cbmDataLimit
.%647! sta cbmDataPtr
.%648! bcc +
.%649! inc cbmDataPtr+1
.%650! + sec
.%651! lda cbmDataLen
.%652! sbc cbmDataLimit
.%653! sta cbmDataLen
.%654! bcs +
.%655! dec cbmDataLen+1
.%656! + lda cbmDataLen
.%657! ora cbmDataLen+1
.%658! bne commieOutMore
If we are finished with the cluster, then clear the CBM Kernal output channel.
.%659! jsr kernelClrchn
.%660! clc
.%661! rts
.%662!
The file copying main routine. Set up for the starting cluster, and call
the cluster copying routine until end-of-file is reached. Checks for a
NULL cluster pointer in the directory entry to handle zero-length files.
.%663! copyFile = * ;( startCluster, lenML, .A=transMode, .X=lfn ) : .CS=err
.%664! ldy #$0e
.%665! sty $ff00
.%666! sta transMode
.%667! stx lfn
.%668! lda startCluster
.%669! ldy startCluster+1
.%670! sta cluster
.%671! sty cluster+1
.%672! jmp +
.%673! - jsr copyFileCluster
.%674! bcc +
.%675! rts
.%676! + lda cluster
.%677! cmp #$ff
.%678! bne -
.%679! lda cluster+1
.%680! cmp #$0f
.%681! bne -
.%682! clc
.%683! rts
.%684!
This is the translation table used to convert from ASCII to PETSCII. You can
modify it to suit your needs if you wish. If you cannot reassemble this file,
then you can sift through the binary file and locate the tabel and change it
there. An entry of $00 means the corresponding ASCII character will not be
translated. You'll notice that I have set up translations for the following
ASCII control characters into PETSCII: Backspace, Tab, Linefeed (CR), and
Formfeed. I also translate the non-PETSCII characters such as {, |, ~, and _
according to what they probably would have been if Commodore wasn't so
concerned with the graphics characters.
.%685! transBuf = *
.%686! ;0 1 2 3 4 5 6 7 8 9 a b c d e f
.%687! .byte $00,$00,$00,$00,$00,$00,$00,$00,$14,$09,$0d,$00,$93,$00,$00,$00 ;0
.%688! .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 ;1
.%689! .byte $20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$2a,$2b,$2c,$2d,$2e,$2f ;2
.%690! .byte $30,$31,$32,$33,$34,$35,$36,$37,$38,$39,$3a,$3b,$3c,$3d,$3e,$3f ;3
.%691! .byte $40,$c1,$c2,$c3,$c4,$c5,$c6,$c7,$c8,$c9,$ca,$cb,$cc,$cd,$ce,$cf ;4
.%692! .byte $d0,$d1,$d2,$d3,$d4,$d5,$d6,$d7,$d8,$d9,$da,$5b,$5c,$5d,$5e,$5f ;5
.%693! .byte $c0,$41,$42,$43,$44,$45,$46,$47,$48,$49,$4a,$4b,$4c,$4d,$4e,$4f ;6
.%694! .byte $50,$51,$52,$53,$54,$55,$56,$57,$58,$59,$5a,$db,$dc,$dd,$de,$df ;7
.%695! .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 ;8
.%696! .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 ;9
.%697! .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 ;a
.%698! .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 ;b
.%699! .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 ;c
.%700! .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 ;d
.%701! .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 ;e
.%702! .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 ;f
.%703!
This is where the track cache, etc. are stored. This section requires 11K of
storage space but does not increase the length of the binary program file
since these storage areas are DEFINED rather than allocated with ".buf"
directives. The Unix terminology for this type of uninitialized data is "bss".
.%704! ;====bss storage====
.%705!
.%706! bss = *
.%707! trackbuf = bss
.%708! clusterBuf = trackbuf+4608
.%709! fatbuf = clusterBuf+1024
.%710! dirbuf = fatbuf+1536
.%711! end = dirbuf+4096
10 rem little red reader, by craig bruce, 30-sep-92, for c= hacking netmag
11 :
These lines set up the default CBM-DOS and MS-DOS device numbers, taking care
to disallow them to be the same device. You can change this to your own drive
configuration.
20 cd=peek(186) : rem ** default cbm-dos drive **
25 dv=9:dt=0 : rem ** ms-dos drive, type (0=1571,255=1581)
26 if dv=cd then dv=8:dt=0 : rem ** alternate ms-dos drive
27 :
30 print chr$(147);"initializing..." : print
40 bank0 : pk=dec("8000")
50 if peek(pk+9)=dec("cb") and peek(pk+10)=131 then 60
55 print"loading machine language routines..." : bload"lrr.bin",u(cd)
60 poke pk+16,dv : poke pk+17,dt : sys pk
I "dim" the following variables before the arrays to avoid the overhead of
pushing the arrays around when creating new scalar variables.
70 dim t,r,b,i,a$,c,dt$,fl$,il$,x,x$
80 dim di$(128),cl(128),sz(128)
90 if dt=255 then dt$="1581" :else dt$="1571"
100 fl$=chr$(19)+chr$(17)+chr$(17)+chr$(17)+chr$(17)
110 il$=fl$:fori=1to19:il$=il$+chr$(17):next
120 goto 500
130 :
131 rem ** load ms-dos directory **
140 print"loading directory..." : print
150 sys pk : sys pk+3
160 dl=0
The "rreg" instruction returns the return values of the .A, .X, .Y, and .S
registers from the last "sys" call. I check the 1-bit of the .S register
(the Carry flag) for error returns.
170 rreg bl,dc,bh,s : e=peek(pk+15)
180 if (s and 1) then gosub 380 : return
190 print"scanning directory..." : print
200 db=bl+256*bh
210 if dc=0 then 360
220 for dp=db to db+32*(dc-1) step 32
230 if peek(dp)=0 or peek(dp)=229 then 350
240 if peek(dp+12) and 24 then 350
250 dl=dl+1
This next line is where I set the default selection status, translation type,
and CBM file type for the MS-DOS files. You can change these defaults simply
by overtyping the string in ( | ||| ||| ) the "V" locations.
V VVV VVV
260 d$=right$(" "+str$(dl),3)+" asc seq " : rem ** default sel/tr/ft **
270 a$="" : fori=0to10 : a$=a$+chr$(peek(dp+i)) : next
280 a$=left$(a$,8)+" "+right$(a$,3)
290 print dl; a$
300 d$=d$+a$+" "
310 cl(dl)=peek(dp+26)+256*peek(dp+27)
320 sz=peek(dp+28)+256*peek(dp+29)+65536*peek(dp+30)
330 di$(dl)=d$+right$(" "+str$(sz),6)
340 sz(dl)=sz
350 next dp
360 return
370 :
371 rem ** report ms-dos disk error **
380 print chr$(18);"ms-dos disk error #";mid$(str$(e),2);
390 print " ($";mid$(hex$(e),3);"), press key.";chr$(146)
400 getkey a$ : return
410 :
411 rem ** screen heading **
420 printchr$(147);"ms-dev=";mid$(str$(dv),2);" ms-type=";dt$;
430 print" cbm-dev=";mid$(str$(cd),2):print
440 return
450 :
451 rem ** screen footing **
460 print il$;"d=directory m=ms-dev f=cbm-dev q=quit"
470 print"t=toggle-column, c=copy-files, +/- page";
480 return
490 :
491 rem ** main routine **
500 t=1 : c=0
510 r=0
520 gosub 420
530 print "num s trn typ filename ext length"
540 print "--- - --- --- -------- --- ------"
550 gosub 460
560 b=t+17 : if b>dl then b=dl
570 print fl$;: if t>dl then 590
580 for i=t to b : print di$(i) : next
590 if dl=0 then print chr$(18);"";chr$(146)
600 if dl=0 then 660
610 print left$(il$,r+5);chr$(18);
620 on c+1 goto 630,640,650
630 print spc(4);mid$(di$(t+r),5,3) : goto 660
640 print spc(7);mid$(di$(t+r),8,5) : goto 660
650 print spc(12);mid$(di$(t+r),13,5) : goto 660
660 getkey a$
Oh shi^Hoot. I screwed up the following line in the string after the
"+chr$(13)+" part. You'll notice that I have avoided putting cursor control
characters into the strings everywhere else, but I forgot to do that here.
The "{stuff}" should be CursorUp, CursorDown, CursorLeft, CursorRight,
CursorHome, and CursorCLR control characters, respectively. These characters
give the index for the "on" statement below.
670 i=instr("dmftc+-q "+chr$(13)+"{stuff}",a$)
680 print left$(il$,r+5);di$(t+r)
690 if i=0 then 600
700 onigoto760,1050,1110,950,1150,1000,1020,730,860,860,770,790,810,830,850,500
710 stop
720 :
721 rem ** various menu options **
730 print chr$(147);"have an awesome day."
740 end
760 gosub 420 : gosub 140 : goto 500
770 r=r-1 : if r<0 then r=b-t
780 goto 600
790 r=r+1 : if t+r>b then r=0
800 goto 600
810 c=c-1 : if c<0 then c=2
820 goto 600
830 c=c+1 : if c>2 then c=0
840 goto 600
850 r=0 : c=0 : goto 600
860 if dl=0 then 600
870 x=t+r : on c+1 gosub 890,910,930
880 print left$(il$,r+5);di$(x) : goto 600
890 if mid$(di$(x),6,1)=" " then x$="*" :else x$=" "
900 mid$(di$(x),6,1)=x$ : return
910 if mid$(di$(x),9,1)="a" then x$="bin" :else x$="asc"
920 mid$(di$(x),9,3)=x$ : return
930 if mid$(di$(x),14,1)="s" then x$="prg" :else x$="seq"
940 mid$(di$(x),14,3)=x$ : return
950 if dl=0 then 600
960 for x=1 to dl
970 on c+1 gosub 890,910,930
980 next x
990 goto 520
1000 if b=dl then t=1 : goto 510
1010 t=t+18 : goto 510
1020 if t=1 then t=dl-(dl-int(dl/18)*18)+1 : goto 510
1030 t=t-18 : if t<1 then t=1
1040 goto 510
1050 print il$;chr$(27);"@";
1060 input"ms-dos device number (8-30)";dv
1061 if cd=dv then print"ms-dos and cbm-dos devices must be different!":goto1060
1070 input"ms-dos device type (71/81)";x
1080 if x=8 or x=81 or x=1581 then dt=255:dt$="1581" :else dt=0:dt$="1571"
1090 poke pk+16,dv : poke pk+17,dt : sys pk
1100 goto 520
1110 print il$;chr$(27);"@";
1120 input "cbm-dos device number (0-30)";cd
1130 if cd=dv then print"ms-dos and cbm-dos devices must be different!":goto1120
1140 goto 520
1141 :
1142 rem ** copy files **
1150 print chr$(147);"copy files":print:print
1160 if dl=0 then fc=0 : goto 1190
1170 fc=0 : for f=1 to dl : if mid$(di$(f),6,1)="*" then gosub 1200
1180 next f
1190 print : print"files copied =";fc;" - press key"
1191 getkey a$ : goto 520
1200 fc=fc+1
1210 x$=mid$(di$(f),19,8)+"."+mid$(di$(f),29,3)
1220 cf$="":fori=1tolen(x$):if mid$(x$,i,1)<>" " then cf$=cf$+mid$(x$,i,1)
1230 next
1231 if right$(cf$,1)="." then cf$=left$(cf$,len(cf$)-1)
1232 cf$=cf$+","+mid$(di$(f),14,1)
1240 print str$(fc);". ";chr$(34);cf$;chr$(34);tab(20);sz(f)"bytes";
1245 print tab(35);mid$(di$(f),9,3)
1250 cl=cl(f) : lb=sz(f) - int(sz(f)/65536)*65536
I had to use a DOPEN statement here for disk files because the regular OPEN
statment does not redirect the DS and DS$ pseudo-variables. You'll notice
that the non-disk OPEN statment below has a secondary address of 7. This is
to put the printer into lowercase mode if you are outputting directly to it.
You can replace this with a 5 (or whatever) if you have a special interface
to an IBM-compatible printer and you want to print directly in ASCII. In this
case, you would select the "BIN" translation mode for the file you are routing
directly to the printer.
1260 if cd>=8 then dopen#1,(cf$+",w"),u(cd) :else if cd<>0 then open 1,cd,7
1265 if cd<8 then 1288
1270 if ds<>63 then 1288
1275 x$="y" : print "file exists; overwrite (y/n)";
1280 close 1 : input x$ : if x$="n" then fc=fc-1 : return
1285 scratch(cf$),u(cd)
1286 dopen#1,(cf$+",w"),u(cd)
1288 if cd<8 then 1320
1300 if ds<20 then 1320
1310 print chr$(18)+"cbm disk error: "+ds$ : fc=fc-1 : close1 : return
1320 poke pk+19,cl/256 : poke pk+18,cl-peek(pk+19)*256
1330 poke pk+21,lb/256 : poke pk+20,lb-peek(pk+21)*256
1340 tr=0 : if mid$(di$(f),9,1)="a" then tr=255
1346 x=1 : if cd=0 then x=0
1350 sys pk+6,tr,x
1355 rreg x,x,x,s : e=peek(pk+15)
1356 if (s and 1) then gosub 380 : fc=fc-1
1360 if cd<>0 and cd<8 then close1
1370 if cd>=8 then dclose#1 : if ds>=20 then 1310
1380 return
begin 640 lrr.128
M`1Q+'`H`CR!,25143$4@4D5$(%)%041%4BP@0ED@0U)!24<@0E)50T4L(#,P
M+5-%4"TY,BP@1D]2($,]($A!0TM)3D<@3D5434%'`%$<"P`Z`(`<%`!#1++"
M*#$X-BD@(#H@CR`J*B!$149!54Q4($-"32U$3U,@1%))5D4@*BH`O!P9`$16
MLCDZ1%2R,"`@.B`@CR`J*B!-4RU$3U,@1%))5D4L(%194$4@*#`],34W,2PR
M-34],34X,2D`\AP:`(L@1%:R0T0@IR!$5K(X.D14LC`@.B"/("HJ($%,5$52
M3D%412!-4RU$3U,@1%))5D4`^!P;`#H`&QT>`)D@QR@Q-#M25I)3DM4$NJ.2FRT2@B0T(B*2"O(,(H4$NJ,3`ILC$S,2"G(#8P`)P=-P"9(DQ/041)
M3D<@34%#2$E.12!,04Y'54%'12!23U5424Y%4RXN+B(@.B#^$2),4E(N0DE.
M(BQ5*$-$*0"_'3P`ER!02ZHQ-BQ$5B`Z()<@4$NJ,3(%!+`.,=
M1@"&(%0L4BQ"+$DL020L0RQ$5"0L1DPD+$E,)"Q8+%@D``(>4`"&($1))"@Q
M,C@I+$-,*#$R."DL4UHH,3(X*0`J'EH`BR!$5+(R-34@IR!$5"2R(C$U.#$B
M(#K5($14)+(B,34W,2(`4!YD`$9,)++'*#$Y*:K'*#$W*:K'*#$W*:K'*#$W
M*:K'*#$W*0!T'FX`24PDLD9,)#J!2;(QI#$Y.DE,)+))3"2JQR@Q-RDZ@@!^
M'G@`B2`U,#``A!Z"`#H`IAZ#`(\@*BH@3$]!1"!-4RU$3U,@1$E214-43U)9
M("HJ`,8>C`"9(DQ/041)3D<@1$E214-43U)9+BXN(B`Z()D`V!Z6`)X@4$L@
M.B">(%!+JC,`X1Z@`$1,LC```!^J`/X)($),+$1#+$)(+%,@.B!%LL(H4$NJ
M,34I`!H?M`"+("A3(*\@,2D@IR"-(#,X,"`Z((X`.Q^^`)DB4T-!3DY)3D<@
M1$E214-43U)9+BXN(B`Z()D`3!_(`$1"LD),JC(U-JQ"2`!='](`BR!$0[(P
M(*<@,S8P`'T?W`"!($10LD1"(*0@1$*J,S*L*$1#JS$I(*D@,S(`G1_F`(L@
MPBA$4"FR,""P(,(H1%`ILC(R.2"G(#,U,`"W'_``BR#"*$10JC$R*2"O(#(T
M(*<@,S4P`,,?^@!$3+)$3*HQ``<@!`%$)++)*"(@(JK$*$1,*2PS*:HB("`@
M("!!4T,@(%-%42`@(B`Z((\@*BH@1$5&055,5"!314PO5%(O1E0@*BH`,B`.
M`4$DLB(B(#H@@4FR,*0Q,"`Z($$DLD$DJLM)++(*$$D+#@IJB(@("*JR2A!)"PS*0!;("(!F2!$3#L@020`;2`L`40DLD0D
MJD$DJB(@("(`CB`V`4-,*$1,*;+"*$10JC(V*:HR-3:LPBA$4*HR-RD`NB!`
M`5-:LL(H1%"J,C@IJC(U-JS"*$10JC(Y*:HV-34S-JS"*$10JC,P*0#;($H!
M1$DD*$1,*;)$)*K)*"(@("`@(JK$*%-:*2PV*0#I(%0!4UHH1$PILE-:`/(@
M7@&"($10`/@@:`&.`/X@<@$Z`",AM2R!%4E)/4B`J*@!0(7P!F2#'*#$X*3LB35,M1$]3($1)4TL@15)23U(@(R([
MRBC$*$4I+#(I.P!](88!F2`B("@D(CO**-(H12DL,RD[(BDL(%!215-3($M%
M62XB.\M3B!(14%$24Y'("HJ`.$AI`&9QR@Q-#M.R(@("`@35,M5%E013TB.T14)#L``B*N`9DB("`@($-"32U$158](CO**,0H
M0T0I+#(I.ID`""*X`8X`#B+"`3H`*2+#`8\@*BH@4T-2145.($9/3U1)3D<@
M*BH`72+,`9D@24PD.R)$/41)4D5#5$]262`@33U-4RU$158@($8]0T)-+41%
M5B!1/5%5250B`(TBU@&9(E0]5$]'1TQ%+4-/3%5-3BP@0SU#3U!9+49)3$53
M+"`K+RT@4$%'12([`),BX`&.`)DBZ@$Z`+(BZP&/("HJ($U!24X@4D]55$E.
M12`J*@#`(O0!5+(Q(#H@0[(P`,@B_@%2LC``TB((`HT@-#(P``(C$@*9(").
M54T@(%,@(%123B`@5%E0("!&24Q%3D%-12`@15A4("!,14Y'5$@B`#(C'`*9
M("(M+2T@("T@("TM+2`@+2TM("`M+2TM+2TM+2`@+2TM("`M+2TM+2TB`#PC
M)@*-(#0V,`!7(S`"0K)4JC$W(#H@BR!"L41,(*<@0K)$3`!P(SH"F2!&3"0[
M.B"+(%2Q1$P@IR`U.3``C2-$`H$@2;)4(*0@0B`Z()D@1$DD*$DI(#H@@@"V
M(TX"BR!$3+(P(*<@F2#'*#$X*3LB/$Y/($9)3$53/B([QR@Q-#8I`,M($1,LC`@IR`V-C``WR-B`ID@R"A)3"0L4JHU*3O'*#$X*3L`]R-L`I$@0ZHQ
M((D@-C,P+#8T,"PV-3``&21V`ID@IC0I.\HH1$DD*%2J4BDL-2PS*2`Z((D@
M-C8P`#LD@`*9(*8W*3O**$1))"A4JE(I+#@L-2D@.B")(#8V,`!?)(H"F2"F
M,3(I.\HH1$DD*%2J4BDL,3,L-2D@.B")(#8V,`!I))0"H?D@020`D"2>`DFR
MU"@B1$U&5$,K+5$@(JK'*#$S*:HBD1&='1.3(BQ!)"D`JB2H`ID@R"A)3"0L
M4JHU*3M$220H5*I2*0"Z)+("BR!)LC`@IR`V,#``!B6\`I%)B3M+#$Q,3`L.34P+#$Q-3`L,3`P,"PQ,#(P+#M.#$P+#@S,"PX-3`L-3`P``PEQ@*0`!(ET`(Z`#,ET0*/("HJ(%9!4DE/55,@
M345.52!/4%1)3TY3("HJ`%M($1!62XB`%TEY`*``'M4JLQ(#H@BR!2LS`@IR!2LD*K5`";)0P#B2`V,#``M246`U*R4JHQ(#H@BR!4
MJE*Q0B"G(%*R,`"_)2`#B2`V,#``UR4J`T.R0ZLQ(#H@BR!#LS`@IR!#LC(`
MX24T`XD@-C`P`/DE/@-#LD.J,2`Z((L@0[$R(*<@0[(P``,F2`.)(#8P,``9
M)E(#4K(P(#H@0[(P(#H@B2`V,#``*B9<`XL@1$RR,""G(#8P,`!*)F8#6+)4
MJE(@.B"1($.J,2"-(#@Y,"PY,3`L.3,P`&HF<`.9(,@H24PD+%*J-2D[1$DD
M*%@I(#H@B2`V,#``E29Z`XL@RBA$220H6"DL-BPQ*;(B("(@IR!8)+(B*B(@
M.M4@6"2R(B`B`*XFA`/**$1))"A8*2PV+#$ILE@D(#H@C@#=)HX#BR#**$1)
M)"A8*2PY+#$ILB)!(B"G(%@DLB)"24XB(#K5(%@DLB)!4T,B`/8FF`/**$1)
M)"A8*2PY+#,ILE@D(#H@C@`F)Z(#BR#**$1))"A8*2PQ-"PQ*;(B4R(@IR!8
M)+(B4%)'(B`ZU2!8)+(B4T51(@!`)ZP#RBA$220H6"DL,30L,RFR6"0@.B".
M`%$GM@.+($1,LC`@IR`V,#``8"?``X$@6+(Q(*0@1$P`>"?*`Y$@0ZHQ((T@
M.#DP+#DQ,"PY,S``@"?4`X(@6`"*)]X#B2`U,C``HR?H`XL@0K)$3""G(%2R
M,2`Z((D@-3$P`+8G\@-4LE2J,3@@.B")(#4Q,`#B)_P#BR!4LC$@IR!4LD1,
MJRA$3*NU*$1,K3$X*:PQ."FJ,2`Z((D@-3$P`/LG!@14LE2K,3@@.B"+(%2S
M,2"G(%2R,0`%*!`$B2`U,3``&B@:!)D@24PD.\M35,M1$]3($1%5DE#12!.54U"15(@*#@M,S`I(CM$5@"%*"4$BR!#1+)$5B"G
M()DB35,M1$]3($%.1"!#0DTM1$]3($1%5DE#15,@35535"!"12!$249&15)%
M3E0A(CJ),3`V,`"J*"X$A2)-4RU$3U,@1$5624-%(%194$4@("@W,2\X,2DB
M.U@`ZR@X!(L@6+(X(+`@6+(X,2"P(%BR,34X,2"G($14LC(U-3I$5"2R(C$U
M.#$B(#K5($14LC`Z1%0DLB(Q-3M,3(%!+`!@I3`2)(#4R,``M*58$F2!)3"0[QR@R-RD[(D`B.P!5
M*6`$A2`B0T)-+41/4R!$159)0T4@3E5-0D52("@P+3,P*2([0T0`FBEJ!(L@
M0T2R1%8@IR"9(DU3+41/4R!!3D0@0T)-+41/4R!$159)0T53($U54U0@0D4@
M1$E&1D5214Y4(2(ZB3$Q,C``I"ET!(D@-3(P`*HI=00Z`,$I=@2/("HJ($-/
M4%D@1DE,15,@*BH`WRE^!)D@QR@Q-#M!(L@1$RR,""G($9#LC`@.B")(#$Q.3``+RJ2!$9#LC`@.B"!($:R,2"D($1,
M(#H@BR#**$1))"A&*2PV+#$ILB(J(B"G((T@,3(P,``W*IP$@B!&`&,JI@29
M(#H@F2)&24Q%4R!#3U!)140@/2([1D,[(B`M(%!215-3($M%62(`=2JG!*'Y
M($$D(#H@B2`U,C``@2JP!$9#LD9#JC$`JBJZ!%@DLLHH1$DD*$8I+#$Y+#@I
MJB(N(JK**$1))"A&*2PR.2PS*0#E*L0$0T8DLB(B.H%)LC&DPRA8)"DZBR#*
M*%@D+$DL,2FSL2(@(B"G($-&)+)#1B2JRBA8)"Q)+#$I`.LJS@2"`!0KSP2+
M(,DH0T8D+#$ILB(N(B"G($-&)++(*$-&)"S#*$-&)"FK,2D`,RO0!$-&)+)#
M1B2J(BPBJLHH1$DD*$8I+#$T+#$I`&M0T8D.\M*$8I+#DL,RD`KROB!$-,LD-,*$8I(#H@3$*R4UHH1BD@JR"U*%-:*$8IK38U
M-3,V*:PV-34S-@#I*^P$BR!#1+&R.""G(/X-(S$L*$-&)*HB+%M*2`ZU2"+($-$L[$P(*<@GR`Q+$-$+#<`^ROQ!(L@0T2S.""G(#$R.#@`#RSV
M!(L@1%.SL38S(*<@,3(X.``^+/L$6"2R(EDB(#H@F2`B1DE,12!%6$E35%,[
M($]615)74DE412`H62].*2([`&8L``6@(#$@.B"%(%@D(#H@BR!8)+(B3B(@
MIR!&0[)&0ZLQ(#H@C@!W+`4%\BA#1B0I+%4H0T0I`)$L!@7^#2,Q+"A#1B2J
M(BQ7(BDL52A#1"D`HRP(!8L@0T2S.""G(#$S,C``MBP4!8L@1%.S,C`@IR`Q
M,S(P`.PL'@69(,M0ZLQ(#H@H#$@.B".`!DM*`67(%!+JC$Y+$-,K3(U-B`Z()<@4$NJ,3@L0TRK
MPBA02ZHQ.2FL,C4V`$8M,@67(%!+JC(Q+$Q"K3(U-B`Z()<@4$NJ,C`L3$*K
MPBA02ZHR,2FL,C4V`&XM/`544K(P(#H@BR#**$1))"A&*2PY+#$ILB)!(B"G
M(%12LC(U-0"%+4(%6+(Q(#H@BR!#1+(P(*<@6+(P`)4M1@6>(%!+JC8L5%(L
M6`"Q+4L%_@D@6"Q8+%@L4R`Z($6RPBA02ZHQ-2D`T2U,!8L@*%,@KR`Q*2"G
M((T@,S@P(#H@1D.R1D.K,0#I+5`%BR!#1+.Q,""O($-$LS@@IR"@,0`.+EH%
IBR!#1+&R.""G(/X/(S$@.B"+($13L;(R,""G(#$S,3``%"YD!8X`````
`
end
begin 640 lrr.bin
M`(!,68%,3H),#H3+@P``````````````````````````````````````2*D`
MA="M$(`@L?^I;R"3_ZE5(*C_)-`P#ZDP(*C_:""H_R30,`(88*D%C0^`.&"M
M`-U)$(T`W6"I""P-W/#[8"!@@*X,W"!7@(I@J1H@*("0`6`@KO\DT##.&"!'
M_RP-W"!7@"!H@(T/@"D/R0*P):``(&B`F2*`R,`&D/48J0@@*("0`6"I`2P1
M@#`"J00@J/\@KO]@2(HI`0H*"@HL$8`0`DD0("B`D`%@:""H_ZD!(*C_J0D@
MJ/\@KO]X&"!'_RP-W"!7@*DXH(6%`H0#J0"%!"P1@#`#(!.!("N!L`GF!*4$
MR0F0ZQA88*DXA0*F!!BIA7TB@84#8``($`8.!`P""B!H@(T/@"D/R0*0`6"B
M`J``J0@L#=SP^ZT`W4D0C0#=K0SM&-((#0$.PA@-`+B)@*&&F%J*DX&&"-((".(8"$!2"^@)`9K0^`*0_)"_`"
M.&`@M(*Z!H@#`"9`(2)CI":AHH@'(8(8+(,"!A0B&"80*I0BF":0*(&B!D`%@A0R$
M#:("H`"Q#)$&R+$,D0;(T/3F#>8'RM#MY@JE"LD*D!*I`84*Y@FE"M`(4)Y@C&"]"[&&`XZ0*P`8BN%H#@`?`'"H0')@>D!QAM'("0`&
M!ZX6@$S3@:D.C0#_J0"@`"#`@2!H@9`!8(4"A`.@#;$"C1:`R0.0!ZD\C0^`
M.&"@$+$"R0+0\:`6L0*-%X#)!+#FH!&Q`HT9@,F!L-M*2DI*C1B`H!.Q`HT:
M@,BQ`HT;@*`8L0+)"="_H!JQ`LD"T+>@#K$"R0'0KZT7@`H8:0&-'8`8;1B`
MC1R`K1J`K!N`..T<@+`!B(T>@(P?@*T6@,D"T`9.'X!N'H"I.*";A0:$!ZD!
MH`"N%X`@TX&0`6"I.*"AA0:$!ZT=@*``KAB`(-.!D`%@J3B@H:X9@!A@A06$
M`T8#:H4$I@,*)@,8902%`HIE`X4#&*4":3B%`J4#:9N%`Z`"L0*9!@"($/BE
M!2D!T`BE!RD/J*4&8*4'H@1&"&K*T/JD"&"E9*1E("6"D`%@J3B@EX5@A&&I
M`(5BK1:`"H5CI62D92`E@X5DA&7)_]`@P`_0'*T4@(5BJ0&N%H#@`?`"J0,M
M%8#0!:X4@/`"A6,@OH-@I@_0`AA@(,G_D`2-#X!@J?^F8]`"I6*%9J``L6`D
M#A`&JKTXA/`#(-+_R,1FT.P8I6!E9H5@D`+F83BE8N5FA6*P`L9CI6(%8]#$
M(,S_&&"@#HP`_X4.A@^M$H"L$X"%9(1E3"J$(&^#D`%@I63)_]#TI67)#]#N
M&&```````````!0)#0"3`````````````````````````"`A(B,D)28G*"DJ
M*RPM+B\P,3(S-#4V-S@Y.CL\/3X_0,'"P\3%QL?(RMV-G:6UQ=7E_`04)#1$5&1TA)2DM,34Y/4%%24U155E=865K;W-W>WP``````
M````````````````````````````````````````````````````````````
M````````````````````````````````````````````````````````````
A````````````````````````````````````````````````
`
end