Skip to navigation

Revs on the BBC Micro

Revs A source

REVS MAIN GAME CODE Produces the binary file Revs.bin that contains the main game code.
Name: Entry [Show more] Type: Subroutine Category: Setup Summary: The main entry point for the game: move code into upper memory and call it Deep dive: The jigsaw puzzle binary
Context: See this subroutine on its own page References: No direct references to this subroutine in this source file
ORG &1200 .Entry LDY #0 \ We start by copying the following block in memory: \ \ * &1200-&12FF is copied to &7900-&79FF \ \ so set up a byte counter in Y .entr1 LDA &1200,Y \ Copy the Y-th byte of &1200 to the Y-th byte of &7900 STA &7900,Y INY \ Increment the byte counter BNE entr1 \ Loop back until we have copied a whole page of bytes JMP SwapCode \ Jump to the routine that we just moved to continue the \ setup process COPYBLOCK &1200, &120E, &7900 \ This code starts out at &1200 and is run \ there, before it duplicates itself to \ &7900-&790D along with the rest of the \ page, so we need to move this code next to \ the block that runs at &790E-&79FF CLEAR &1200, &120E \ We also need to clear the code from &1200 so we can \ assemble more code at the same location
Name: SwapCode [Show more] Type: Subroutine Category: Setup Summary: Move the track data to the right place and run a checksum on it Deep dive: The jigsaw puzzle binary
Context: See this subroutine on its own page References: This subroutine is called as follows: * Entry calls SwapCode
ORG &790E .SwapCode LDA #200 \ Call OSBYTE with A = 200, X = 3 and Y = 0 to disable LDX #3 \ the ESCAPE key and clear memory if the BREAK key is LDY #0 \ pressed JSR OSBYTE LDA #140 \ Call OSBYTE with A = 140 and X = 0 to select the tape LDX #0 \ filing system (i.e. do a *TAPE command) JSR OSBYTE \ We now want to move the track data from trackLoad \ (which is where the loading process loads the track \ file) to trackData (which is where the game expects \ to find the track data) \ \ At the same time, we also want to move the data that \ is currently at trackData, which is part of the \ dashboard image, into screen memory at trackLoad \ \ trackLoad is &70DB and trackData is &5300, so we \ want to do the following: \ \ * Swap &70DB-&77FF and &5300-&5A24 \ \ At the same time, we want to perform a checksum on the \ track data and compare the results with the four \ checksum bytes in trackChecksum \ \ The following does this in batches of 256 bytes, using \ Y as an index that goes from 0 to 255. The checks are \ done at the end of the loop, and they check the value \ of Y first (against &25), and then the high byte of \ the higher address (against &77) but only if the Y \ test fails, so the swaps end up being: \ \ * Swap &5300 + 0-255 with &70DB + 0-255 \ * Swap &5400 + 0-255 with &71DB + 0-255 \ * Swap &5500 + 0-255 with &72DB + 0-255 \ * Swap &5600 + 0-255 with &73DB + 0-255 \ * Swap &5700 + 0-255 with &74DB + 0-255 \ * Swap &5800 + 0-255 with &75DB + 0-255 \ * Swap &5900 + 0-255 with &76DB + 0-255 \ * Swap &5A00 + 0-&24 with &77DB + 0-&24 \ \ So the last operation swaps &5A24 and &77FF LDA #LO(trackData) \ Set (Q P) = trackData STA P \ LDA #HI(trackData) \ so that's one address for the swap STA Q LDA #LO(trackLoad) \ Set (S R) = trackLoad STA R \ LDA #HI(trackLoad) \ so that's the other address for the swap STA S LDY #0 \ Set a byte counter in Y for the swap .swap1 LDA (R),Y \ Swap the Y-th bytes of (Q P) and (S R) PHA LDA (P),Y STA (R),Y PLA STA (P),Y AND #3 \ Decrement the relevant checksum byte TAX \ DEC trackChecksum,X \ The checksum bytes work like this: \ \ * trackChecksum+0 counts the number of data bytes \ ending in %00 \ \ * trackChecksum+1 counts the number of data bytes \ ending in %01 \ \ * trackChecksum+2 counts the number of data bytes \ ending in %10 \ \ * trackChecksum+3 counts the number of data bytes \ ending in %11 \ \ This code checks off the relevant checksum byte for \ the data byte in A, so if all the data is correct, \ this will eventually decrement all four bytes to zero INY \ Increment the loop counter BNE swap2 \ If we have finished swapping a whole page of bytes, INC Q \ increment the high bytes of (Q P) and (S R) to move INC S \ on to the next page .swap2 CPY #&25 \ If we have not yet reached addresses &5A24 and &77FF, BNE swap1 \ jump back to swap1 to keep swapping data LDA S CMP #&77 BNE swap1 LDX #3 \ The data swap is now done, so we now check that all \ three checksum bytes at trackChecksum are zero, so set \ a counter in X to work through the four bytes .swap3 LDA trackChecksum,X \ If the X-th checksum byte is non-zero, the checksum BNE swap4 \ has failed, so jump to swap4 to reset the machine DEX \ Decrement the checksum byte counter BPL swap3 \ Loop back to check the next checksum byte BMI MoveCode \ If we get here then all four checksum bytes are zero, \ so jump to swap4 to keep going (this BMI is \ effectively a JMP as we just passed through a BPL) .swap4 JMP (&FFFC) \ The checksum has failed, so reset the machine
Name: MoveCode [Show more] Type: Subroutine Category: Setup Summary: Move and reset various blocks around in memory Deep dive: The jigsaw puzzle binary
Context: See this subroutine on its own page References: This subroutine is called as follows: * SwapCode calls MoveCode
.MoveCode \ We are going to process the five memory blocks defined \ in (blockStartHi blockStartLo)-(blockEndHi blockEndLo) \ \ We will either zero the memory block (for the first \ block in the table), or move the block to the address \ in (blockToHi blockToLo) \ \ We work through the blocks from the last entry to the \ first, so we end up doing this: \ \ * Move &1500-&15DA to &7000-&70DA \ * Move &1300-&14FF to &0B00-&0CFF \ * Move &5A80-&645B to &0D00-&16DB \ * Move &64D0-&6BFF to &5FD0-&63FF \ * Zero &5A80-&5E3F LDX #4 \ Set a block counter in X to work through the five \ memory blocks, starting with the block defined at \ the end of the block tables LDY #0 \ Set Y as a byte counter .move1 LDA blockStartLo,X \ Set (Q P) to the X-th address from (blockStartHi STA P \ blockStartLo) LDA blockStartHi,X STA Q LDA blockToLo,X \ Set (S R) to the X-th address from (blockToHi STA R \ blockToLo) LDA blockToHi,X STA S .move2 LDA (P),Y \ Copy the Y-th byte of (Q P) to the Y-th byte of (S R) STA (R),Y \ \ The LDA (P),Y instruction gets modified to LDA #0 for \ the last block that we process, i.e. when X = 0 INC P \ Increment the address in (Q P), starting with the low \ byte BNE move3 \ Increment the high byte if we cross a page boundary INC Q .move3 INC R \ Increment the address in (S R), starting with the low \ byte BNE move4 \ Increment the high byte if we cross a page boundary INC S .move4 LDA P \ If (Q P) <> (blockEndHi blockEndLo) then jump back to CMP blockEndLo,X \ move2 to process the next byte in the block BNE move2 LDA Q CMP blockEndHi,X BNE move2 DEX \ We have finished processing a block, so decrement the \ block counter in X to move on to the next block (i.e. \ the previous entry in the table) BMI move5 \ If X < 0 then we have finished processing all five \ blocks, so jump to move5 BNE move1 \ If X <> 0, i.e. X > 0, then jump up to move1 to move \ the next block LDA ldaZero \ We get here when X = 0, which means we have reached STA move2 \ the last block to process (i.e. the first entry in the LDA ldaZero+1 \ block tables) STA move2+1 \ \ We don't want to copy this block, we want to zero it, \ so we modify the instruction at move2 to LDA #0, so \ the code zeroes the block rather than moving it JMP move1 \ Jump back to move1 to zero the final block .move5 IF _ACORNSOFT OR _4TRACKS JMP SetupGame \ If we get here we have processed all the blocks in the \ block tables, so jump to SetupGame to continue setting \ up the game ELIF _SUPERIOR OR _REVSPLUS JMP Protect \ If we get here we have processed all the blocks in the \ block tables, so jump to Protect to continue setting \ up the game ENDIF
Name: ldaZero [Show more] Type: Variable Category: Setup Summary: Contains code that's used for modifying the MoveCode routine
Context: See this variable on its own page References: This variable is used as follows: * MoveCode uses ldaZero
.ldaZero LDA #0 \ The instruction at move2 in the MoveCode routine is \ modified to this instruction so the routine zeroes a \ block of memory rather than moving it
Name: blockStartLo [Show more] Type: Variable Category: Setup Summary: Low byte of the start address of blocks moved by the MoveCode routine
Context: See this variable on its own page References: This variable is used as follows: * MoveCode uses blockStartLo
.blockStartLo EQUB &80, &D0 EQUB &80, &00 EQUB &00
Name: blockStartHi [Show more] Type: Variable Category: Setup Summary: High byte of the start address of blocks moved by the MoveCode routine
Context: See this variable on its own page References: This variable is used as follows: * MoveCode uses blockStartHi
.blockStartHi EQUB &5A, &64 EQUB &5A, &13 EQUB &15
Name: blockEndLo [Show more] Type: Variable Category: Setup Summary: Low byte of the end address of blocks moved by the MoveCode routine
Context: See this variable on its own page References: This variable is used as follows: * MoveCode uses blockEndLo
.blockEndLo EQUB &40, &00 EQUB &5C, &00 EQUB &DB
Name: blockEndHi [Show more] Type: Variable Category: Setup Summary: High byte of the end address of blocks moved by the MoveCode routine
Context: See this variable on its own page References: This variable is used as follows: * MoveCode uses blockEndHi
.blockEndHi EQUB &5E, &6C EQUB &64, &15 EQUB &15
Name: blockToLo [Show more] Type: Variable Category: Setup Summary: Low byte of the destination address of blocks moved by the MoveCode routine
Context: See this variable on its own page References: This variable is used as follows: * MoveCode uses blockToLo
.blockToLo EQUB &80, &D0 EQUB &00, &00 EQUB &00
Name: blockToHi [Show more] Type: Variable Category: Setup Summary: High byte of the destination address of blocks moved by the MoveCode routine
Context: See this variable on its own page References: This variable is used as follows: * MoveCode uses blockToHi
.blockToHi EQUB &5A, &5F EQUB &0D, &0B EQUB &70 EQUB &09, &B9 \ These bytes appear to be unused EQUB &02, &50 EQUB &9D, &01 EQUB &09, &9D EQUB &79, &09 EQUB &B9, &03 EQUB &50, &9D EQUB &02, &09 EQUB &B9, &01 EQUB &51, &9D EQUB &00, &0A EQUB &B9, &02 EQUB &51, &9D EQUB &01, &0A EQUB &9D, &79 EQUB &0A, &B9 EQUB &03, &51 EQUB &9D, &02 EQUB &0A, &B9 EQUB &04, &50 EQUB &9D, &78 EQUB &09, &B9 EQUB &06, &50 EQUB &9D, &7A EQUB &09, &B9 EQUB &04
Name: soundData [Show more] Type: Variable Category: Sound Summary: OSWORD blocks for making the various game sounds Deep dive: The engine sounds
Context: See this variable on its own page References: This variable is used as follows: * MakeDrivingSounds uses soundData * MakeSound uses soundData * MakeSoundEnvelope uses soundData

Sound data. To make a sound, the MakeSound passes the bytes in this table to OSWORD 7. These bytes are the OSWORD equivalents of the parameters passed to the SOUND keyword in BASIC. The parameters have these meanings: channel/flush, amplitude (or envelope number if 1-4), pitch, duration where each value consists of two bytes, with the low byte first and the high byte second. For the channel/flush parameter, the top nibble of the low byte is the flush control (where a flush control of 0 queues the sound, and a flush control of 1 makes the sound instantly), while the bottom nibble of the low byte is the channel number. When written in hexadecimal, the first figure gives the flush control, while the second is the channel (so &13 indicates flush control = 1 and channel = 3).
ORG &0B00 EQUB &10, &10 \ These bytes appear to be unused EQUB &10, &10 EQUB &10, &10 EQUB &10, &10 EQUB &10, &10 EQUB &10, &10 EQUB &10, &10 EQUB &10, &10 .soundData EQUB &10, &00 \ Sound #0: Engine exhaust (SOUND &10, -10, 3, 255) EQUB &F6, &FF EQUB &03, &00 EQUB &FF, &00 EQUB &11, &00 \ Sound #1: Engine tone 1 (SOUND &11, -10, 187, 255) EQUB &F6, &FF EQUB &BB, &00 EQUB &FF, &00 EQUB &12, &00 \ Sound #2: Engine tone 2 (SOUND &12, -10, 40, 255) EQUB &F6, &FF EQUB &28, &00 EQUB &FF, &00 EQUB &13, &00 \ Sound #3: Tyre squeal (SOUND &13, 1, 130, 255) EQUB &01, &00 EQUB &82, &00 EQUB &FF, &00 EQUB &10, &00 \ Sound #4: Crash/contact (SOUND &10, -10, 6, 4) EQUB &F6, &FF EQUB &06, &00 EQUB &04, &00
Name: envelopeData [Show more] Type: Variable Category: Sound Summary: Data for the sound envelope for squealing tyres Deep dive: The engine sounds
Context: See this variable on its own page References: This variable is used as follows: * DefineEnvelope uses envelopeData * ProcessShiftedKeys uses envelopeData

There is only one sound envelope defined in Revs: * Envelope 1 defines the sound of the tyres squealing
.envelopeData EQUB 1, 1, 2, -2, -6, 4, 1, 1, 10, 0, 0, 0, 72, 0
Name: xStoreSound [Show more] Type: Variable Category: Sound Summary: Temporary storage for X so it can be preserved through calls to the sound routines
Context: See this variable on its own page References: This variable is used as follows: * DefineEnvelope uses xStoreSound * MakeSound uses xStoreSound * MakeSoundEnvelope uses xStoreSound
.xStoreSound EQUB &FF
Name: MakeSound [Show more] Type: Subroutine Category: Sound Summary: Make a sound Deep dive: The engine sounds
Context: See this subroutine on its own page References: This subroutine is called as follows: * ApplyTyresAndSkids calls MakeSound * MakeDrivingSounds calls MakeSound * ApplyBounce calls via MakeSound-3 * CheckForCrash calls via MakeSound-3 * MakeDrivingSounds calls via MakeSound-3 * SquealTyres calls via MakeSound-3

Arguments: A The sound number from the soundData table (0 to 4) Y The volume level to use for the sound, or the envelope number (the latter is used for sound #3 only, and is always set to envelope 1, which is the only envelope)
Other entry points: MakeSound-3 Make the sound at the current volume level
LDY volumeLevel \ Set Y to the current volumeLevel, to use as the sound \ amplitude below .MakeSound STX xStoreSound \ Store the value of X in xStoreSound, so we can \ preserve it through the call to the MakeSound routine ASL A \ Set A = A * 8 ASL A \ ASL A \ so we can use it as an index into the soundData table, \ which has 8 bytes per entry CLC \ Set (Y X) = soundData + A ADC #LO(soundData) \ TAX \ starting with the low byte in X, which gets set to the \ following, as LO(soundData) is 16: \ \ * 16 for sound #0 \ * 24 for sound #1 \ * 32 for sound #2 \ * 40 for sound #3 \ * 48 for sound #4 \ \ This means that soundData - 16 + X points to the sound \ data block for the sound we are making, which we now \ use to set the volume or envelope for the sound to Y, \ and flag the correct sound buffer as being in use TYA \ Set byte #2 of the sound data (low byte of amplitude STA soundData-16+2,X \ or envelope number) to Y LDA soundData-16,X \ Set Y to byte #0 of the sound data (channel/flush), AND #3 \ and extract the channel number into Y TAY LDA #7 \ Set A = 7 for the OSWORD command to make a sound STA soundBuffer,Y \ Set the Y-th sound buffer status to 7, which is \ non-zero and indicates that we are making a sound on \ this channel BNE MakeSoundEnvelope \ Jump to MakeSoundEnvelope to set up Y and apply the \ OSWORD command to the (Y X) block, which makes the \ relevant sound (this BNE is effectively a JMP as A is \ never zero)
Name: DefineEnvelope [Show more] Type: Subroutine Category: Sound Summary: Define a sound envelope
Context: See this subroutine on its own page References: This subroutine is called as follows: * ProcessShiftedKeys calls DefineEnvelope * ResetVariables calls DefineEnvelope

Arguments: A The offset of the sound envelope data in envelopeData: * A = 0 for the first (and only) envelope definition
Returns: X X is unchanged
.DefineEnvelope STX xStoreSound \ Store the value of X in xStoreSound, so we can \ preserve it through the call to the DefineEnvelope \ routine CLC \ Set (Y X) = envelopeData + A ADC #LO(envelopeData) \ TAX \ starting with the low byte LDA #8 \ Set A = 8 for the OSWORD command to define an envelope \ Fall through into MakeSoundEnvelope to set up Y and \ apply the OSWORD command to the (Y X) block, which \ defines the relevant sound envelope
Name: MakeSoundEnvelope [Show more] Type: Subroutine Category: Sound Summary: Either make a sound or set up an envelope
Context: See this subroutine on its own page References: This subroutine is called as follows: * MakeSound calls MakeSoundEnvelope

Arguments: A The action: * A = 7 make a sound * A = 8 to define a sound envelope X The low byte of the address of the OSWORD block xStoreSound The value of X to restore at the end of the routine
.MakeSoundEnvelope LDY #HI(soundData) \ Set y to the high byte of the soundData block \ address, so (Y X) now points to the relevant envelope \ or sound data block JSR OSWORD \ Call OSWORD with action A, as follows: \ \ * A = 7 to make the sound at (Y X) \ \ * A = 8 to set up the sound envelope at (Y X) LDX xStoreSound \ Restore the value of X we stored before calling the \ routine, so it doesn't change RTS \ Return from the subroutine
Name: ScaleWingSettings [Show more] Type: Subroutine Category: Driving model Summary: Scale the wing settings and calculate the wing balance, for use in the driving model
Context: See this subroutine on its own page References: This subroutine is called as follows: * MainDrivingLoop (Part 1 of 5) calls ScaleWingSettings

The wing settings (0 to 40) are scaled to the range 90 to 218. The wing balance is calculated as: 60 + (rearWingSetting * 3 + frontWingSetting) / 2 which is in the range 60 to 140, with higher numbers when the rear wing is greater (i.e. pushes down more) than the front wing.
.ScaleWingSettings LDX #1 \ We are about to loop through the two wings, so set a \ counter in X so we do the rear wing setting first, and \ then the front wing setting .wing1 LDA frontWingSetting,X \ Set U = wing setting * 4 ASL A \ ASL A \ where the wing setting was entered by the player STA U LDA wingScaleFactor,X \ Set A to the wingScaleFactor for this wing, which is \ hard-coded to 205 for both wings JSR Multiply8x8 \ Set (A T) = A * U \ So by this point, we have: \ \ A = A * U / 256 \ = U * 205 / 256 \ = wing setting * 4 * 205 / 256 \ = wing setting * 820 / 256 \ \ The wing settings can be from 0 to 40, so this scales \ the setting to the range 0 to 128 CLC \ Set A = A + 90 ADC #90 \ \ which is in the range 90 to 218 STA wingSetting,X \ Store the scaled wing setting in wingSetting DEX \ Decrement the loop counter BPL wing1 \ Loop back until we have scaled both wing settings LDA rearWingSetting \ Set A = (rearWingSetting * 2 + rearWingSetting ASL A \ + frontWingSetting) / 2 + 60 ADC rearWingSetting \ = (rearWingSetting * 3 + frontWingSetting) / 2 ADC frontWingSetting \ + 60 LSR A \ ADC #60 \ which is in the range 60 to 140, with higher numbers \ when the rear wing is greater than the front wing STA wingBalance \ Store the wing balance in wingBalance RTS \ Return from the subroutine
Name: wingScaleFactor [Show more] Type: Variable Category: Driving model Summary: Scale factors for the wing settings
Context: See this variable on its own page References: This variable is used as follows: * ScaleWingSettings uses wingScaleFactor
.wingScaleFactor EQUB 205 \ Scale factor for the front wing setting EQUB 205 \ Scale factor for the rear wing setting
Name: SpinTrackSection [Show more] Type: Subroutine Category: Track geometry Summary: Apply spin to a section in the track section list
Context: See this subroutine on its own page References: This subroutine is called as follows: * GetSectionAngles (Part 1 of 3) calls SpinTrackSection

Arguments: Y The index of the entry in the track section list: Y = 0 to 5 (update the right verge): * Reset vergeDataRight to zero * Subtract spinYawAngle from the yaw angles in xVergeRightLo, xVergeRightHi * Subtract spinPitchAngle from the pitch angle in yVergeRight * Update horizonListIndex and horizonLine Y = 0 to 5 + 40 (update the left verge): * Reset vergeDataLeft to zero * Subtract spinYawAngle from the yaw angles in xVergeLeftLo, xVergeLeftHi * Subtract spinPitchAngle from the pitch angle in yVergeLeft * Update horizonListIndex and horizonLine
.SpinTrackSection LDA #0 \ Set the Y-th entry in vergeDataRight to 0, to reset STA vergeDataRight,Y \ the colour of the verge mark to black (this is \ recalculated in the GetVergeAndMarkers routine) LDA xVergeRightLo,Y \ Set xVergeRight = xVergeRight - spinYawAngle SEC \ SBC spinYawAngleHi \ starting with the low bytes STA xVergeRightLo,Y LDA xVergeRightHi,Y \ And then the high bytes SBC spinYawAngleTop STA xVergeRightHi,Y LDA yVergeRight,Y \ Set A = Y-th entry in yVergeRight - spinPitchAngle SEC SBC spinPitchAngle STA yVergeRight,Y \ Store the result in the Y-th entry in yVergeRight CMP horizonLine \ If A < horizonLine, then this track section is lower BCC rott1 \ than the current horizon, so jump to rott1 to return \ from the subroutine STA horizonLine \ Otherwise this track section is higher than the \ current horizon pitch angle, so the track obscures the \ horizon and we need to update horizonLine to this \ new pitch angle STY horizonListIndex \ Set horizonListIndex to the track section list index \ number in Y .rott1 RTS \ Return from the subroutine
Name: AddVectors [Show more] Type: Subroutine Category: Maths (Geometry) Summary: Add two three-axis vectors together
Context: See this subroutine on its own page References: This subroutine is called as follows: * BuildCarObjects (Part 3 of 3) calls AddVectors * GetTrackSegment (Part 3 of 3) calls AddVectors

Given a three-axis variable vectorX and a three-axis variable vectorY, this routine calculates the following addition: [ (SS T) ] vectorX = vectorY + [ (TT U) ] [ (UU V) ]
Arguments: X The offset of the vectorX variable to update: * Index * 3 of the track segment to use for the section coordinates for the inner track * &F4 = xHelmetCoord * &FA = xCoord1 * &FD = xCoord2 Y The offset of the vectorY variable to add: * Index * 3 of the track segment to use for the section coordinates for the inner track * &F4 = xHelmetCoord * &FA = xCoord1 * &FD = xCoord2 (SS T) The value to add to the first axis (TT U) The value to add to the second axis (UU V) The value to add to the third axis
.AddVectors LDA xSegmentCoordILo,Y \ Set xVectorX = xVectorY + (SS T) CLC \ ADC T \ starting with the low bytes STA xSegmentCoordILo,X LDA xSegmentCoordIHi,Y \ And then the high bytes ADC SS STA xSegmentCoordIHi,X LDA ySegmentCoordILo,Y \ Set yVectorX = yVectorY + (TT U) CLC \ ADC U \ starting with the low bytes STA ySegmentCoordILo,X LDA ySegmentCoordIHi,Y \ And then the high bytes ADC TT STA ySegmentCoordIHi,X LDA zSegmentCoordILo,Y \ Set zVectorX = zVectorY + (UU V) CLC \ ADC V \ starting with the low bytes STA zSegmentCoordILo,X LDA zSegmentCoordIHi,Y \ And then the high bytes ADC UU STA zSegmentCoordIHi,X RTS \ Return from the subroutine
Name: Multiply8x8 [Show more] Type: Subroutine Category: Maths (Arithmetic) Summary: Calculate (A T) = T * U
Do the following multiplication of two unsigned 8-bit numbers: (A T) = A * U
Returns: X X is unchanged
Other entry points: Multiply8x8+2 Calculate (A T) = T * U
.Multiply8x8 STA T \ Set T = A \ We now calculate (A T) = T * U \ = A * U LDA #0 \ Set A = 0 so we can start building the answer in A LSR T \ Set T = T >> 1 \ and C flag = bit 0 of T \ We are now going to work our way through the bits of \ T, and do a shift-add for any bits that are set, \ keeping the running total in A, and instead of using a \ loop, we unroll the calculation, starting with bit 0 BCC P%+5 \ If C (i.e. the next bit from T) is set, do the CLC \ addition for this bit of T: ADC U \ \ A = A + U ROR A \ Shift A right to catch the next digit of our result, \ which the next ROR sticks into the left end of T while \ also extracting the next bit of T ROR T \ Add the overspill from shifting A to the right onto \ the start of T, and shift T right to fetch the next \ bit for the calculation into the C flag BCC P%+5 \ Repeat the shift-and-add loop for bit 1 CLC ADC U ROR A ROR T BCC P%+5 \ Repeat the shift-and-add loop for bit 2 CLC ADC U ROR A ROR T BCC P%+5 \ Repeat the shift-and-add loop for bit 3 CLC ADC U ROR A ROR T BCC P%+5 \ Repeat the shift-and-add loop for bit 4 CLC ADC U ROR A ROR T BCC P%+5 \ Repeat the shift-and-add loop for bit 5 CLC ADC U ROR A ROR T BCC P%+5 \ Repeat the shift-and-add loop for bit 6 CLC ADC U ROR A ROR T BCC P%+5 \ Repeat the shift-and-add loop for bit 7 CLC ADC U ROR A ROR T RTS \ Return from the subroutine
Name: Divide8x8 [Show more] Type: Subroutine Category: Maths (Arithmetic) Summary: Calculate T = 256 * A / V
Context: See this subroutine on its own page References: This subroutine is called as follows: * GetObjPitchAngle calls Divide8x8 * GetObjYawAngle (Part 2 of 4) calls Divide8x8 * GetObjYawAngle (Part 4 of 4) calls Divide8x8

In the same way that shift-and-add implements a binary version of the manual long multiplication process, shift-and-subtract implements long division. We shift bits out of the left end of the number being divided (A), subtracting the largest possible multiple of the divisor (V) after each shift; each bit of A where we can subtract Q gives a 1 the answer to the division, otherwise it gives a 0.
Arguments: T This has something to do with rounding the result A Unsigned integer V Unsigned integer
.Divide8x8 ASL T \ Shift T left, which clears bit 0 of T, ready for us to \ start building the result \ We now repeat the following five instruction block \ eight times, one for each bit in T ROL A \ Shift A to the left to extract the next bit from the \ number being divided BCS P%+6 \ If we just shifted a 1 out of A, skip the next two \ instructions and jump straight to the subtraction CMP V \ If A < V skip the following two instructions with the BCC P%+5 \ C flag clear, so we shift a 0 into the result in T SBC V \ A >= V, so set A = A - V and set the C flag so we SEC \ shift a 1 into the result in T ROL T \ Shift T to the left, pulling the C flag into bit 0 ROL A \ Repeat the shift-and-subtract loop for bit 1 BCS P%+6 CMP V BCC P%+5 SBC V SEC ROL T ROL A \ Repeat the shift-and-subtract loop for bit 2 BCS P%+6 CMP V BCC P%+5 SBC V SEC ROL T ROL A \ Repeat the shift-and-subtract loop for bit 3 BCS P%+6 CMP V BCC P%+5 SBC V SEC ROL T ROL A \ Repeat the shift-and-subtract loop for bit 4 BCS P%+6 CMP V BCC P%+5 SBC V SEC ROL T ROL A \ Repeat the shift-and-subtract loop for bit 5 BCS P%+6 CMP V BCC P%+5 SBC V SEC ROL T ROL A \ Repeat the shift-and-subtract loop for bit 6 BCS P%+6 CMP V BCC P%+5 SBC V SEC ROL T ROL A \ Repeat the shift-and-subtract loop for bit 7, but BCS P%+4 \ without the subtraction, as we don't need to keep CMP V \ calculating A once its top bit has been extracted ROL T RTS \ Return from the subroutine
Name: GetObjectDistance [Show more] Type: Subroutine Category: 3D objects Summary: Calculate the distance between an object and the player's car, for collision purposes
Context: See this subroutine on its own page References: This subroutine is called as follows: * CheckForContact calls GetObjectDistance * GetSectionYawAngle calls GetObjectDistance

This routine is called with the smaller yaw angle of the object, where 0 to 255 represents 0 to 45 degrees, so 103 = 18.2 degrees. The smaller viewing angle is taken from the arctan calculation for the yaw angle calculation in GetObjYawAngle, so that's the triangle whose hypotenuse is the line between the player and the object, and whose other sides are parallel to the x-axis and z-axis. If the smaller yaw angle is < 18.2 degrees, the routine does this: * Set (L K) = (J I) + (H G) / 8 = max + min / 8 If the smaller yaw angle is >= 18.2 degrees, the routine does this: * Set (L K) = (J I) * 7/8 + (H G) / 2 = max * 7/8 + min / 2 This appears to set the distance between the object and the player's car, for the purposes of determining whether contact has been made. I suspect the calculation is an approximation of Pythagoras that is much faster to calculate, split into small yaw angles (when the objects are close to being orthogonal to each other) and larger yaw angles (when their relative positions are closer to the diagonal). This is a guess, though.
Arguments: (J I) max(|x-delta|, |z-delta|) (H G) min(|x-delta|, |z-delta|) M The smaller yaw angle of the object, where 0 to 255 represents 0 to 45 degrees, so 103 = 18.2 degrees
Returns: (L K) The distance between the object and the player's car A Contains the high byte of (L K)
.GetObjectDistance LDA M \ If M >= 103, jump to odis1 CMP #103 BCS odis1 LDA G \ Set A = G LSR H \ Set (H A) = (H A) >> 3 ROR A \ = (H G) >> 3 LSR H ROR A LSR H ROR A CLC \ Set (L K) = (J I) + (H A) ADC I \ = (J I) + (H G) >> 3 STA K \ = (J I) + (H G) / 8 LDA H ADC J STA L RTS \ Return from the subroutine .odis1 LSR H \ Set (H G) = (H G) >> 1 ROR G LDA J \ Set (T A) = (J I) STA T LDA I LSR T \ Set (T U) = (T A) >> 3 ROR A \ = (J I) >> 3 LSR T ROR A LSR T ROR A STA U LDA G \ Set (L K) = (J I) + (H G) CLC \ = (J I) + (H G) >> 1 ADC I STA K LDA H ADC J STA L LDA K \ Set (L K) = (L K) - (T U) SEC \ = (J I) + (H G) >> 1 - (J I) >> 3 SBC U \ = (J I) * 7/8 + (H G) / 2 STA K LDA L SBC T STA L RTS \ Return from the subroutine EQUB &F1, &0C \ These bytes appear to be unused EQUB &E5, &74 EQUB &8D, &F6 EQUB &0C, &60 EQUB &00, &00 EQUB &00, &00 EQUB &00, &00 EQUB &40
Name: GetRotationMatrix (Part 1 of 5) [Show more] Type: Subroutine Category: Maths (Geometry) Summary: Calculate the rotation matrix for rotating the player's yaw angle into the global 3D coordinate system Deep dive: The core driving model Trigonometry
Context: See this subroutine on its own page References: This subroutine is called as follows: * ApplyDrivingModel calls GetRotationMatrix

This routine calculates the following: sinYawAngle = sin(playerYawAngle) cosYawAngle = cos(playerYawAngle) We can use these to create a rotation matrix that rotates the yaw angle from the player's frame of reference into the global 3D coordinate system.
Arguments: (A X) Player yaw angle in (playerYawAngleHi playerYawAngleLo)
.GetRotationMatrix STA J \ Set (J T) = (A X) STX T \ = playerYawAngle JSR GetAngleInRadians \ Set (U A) to the playerYawAngle, reduced to a quarter \ circle, converted to radians, and halved \ \ Let's call this yawRadians / 2, where yawRadians is \ the reduced player yaw angle in radians STA G \ Set (U G) = (U A) = yawRadians / 2 LDA U \ Set (A G) = (U G) = yawRadians / 2 STA H \ Set (H G) = (A G) = yawRadians / 2 \ So we now have: \ \ (H G) = (A G) = (U G) = yawRadians / 2 \ \ This is the angle vector that we now project onto the \ x- and z-axes of the world 3D coordinate system LDX #1 \ Set X = 0 and secondAxis = 1, so we project sin(H G) STX secondAxis \ into sinYawAngle and cos(H G) into cosYawAngle LDX #0 BIT J \ If bit 6 of J is clear, then playerYawAngle is in one BVC rotm1 \ of these ranges: \ \ * 0 to 63 (%00000000 to %00111111) \ \ * -128 to -65 (%10000000 to %10111111) \ \ The degree system in Revs looks like this: \ \ 0 \ -32 | +32 Overhead view of car \ \ | / \ \ | / 0 = looking straight ahead \ \|/ +64 = looking sharp right \ -64 -----+----- +64 -64 = looking sharp left \ /|\ \ / | \ \ / | \ \ -96 | +96 \ 128 \ \ So playerYawAngle is in the top-right or bottom-left \ quarter in the above diagram \ \ In both cases we jump to rotm1 to set sinYawAngle and \ cosYawAngle \ If we get here then bit 6 of J is set, so \ playerYawAngle is in one of these ranges: \ \ * 64 to 127 (%01000000 to %01111111) \ \ * -64 to -1 (%11000000 to %11111111) \ \ So playerYawAngle is in the bottom-right or top-left \ quarter in the above diagram \ \ In both cases we set the variables the other way \ round, as the triangle we draw to calculate the angle \ is the opposite way round (i.e. it's reflected in the \ x-axis or y-axis) INX \ Set X = 1 and secondAxis = 0, so we project sin(H G) DEC secondAxis \ into cosYawAngle and cos(H G) into sinYawAngle \ We now enter a loop that sets sinYawAngle + X to \ sin(H G) on the first iteration, and sets \ sinYawAngle + secondAxis to cos(H G) on the second \ iteration \ \ The commentary is for the sin(H G) iteration, see the \ end of the loop for details of how the second \ iteration calculates cos(H G) instead .rotm1 \ If we get here, then we are set up to calculate the \ following: \ \ * If playerYawAngle is top-right or bottom-left: \ \ sinYawAngle = sin(playerYawAngle) \ cosYawAngle = cos(playerYawAngle) \ \ * If playerYawAngle is bottom-right or top-left: \ \ sinYawAngle = cos(playerYawAngle) \ cosYawAngle = sin(playerYawAngle) \ \ In each case, the calculation gives us the correct \ coordinate, as the second set of results uses angles \ that are "reflected" in the x-axis or y-axis by the \ capping process in the GetAngleInRadians routine CMP #122 \ If A < 122, i.e. U < 122 and H < 122, jump to rotm2 BCC rotm2 \ to calculate sin(H G) for smaller angles BCS rotm3 \ Jump to rotm3 to calculate sin(H G) for larger angles \ (this BCS is effectively a JMP as we just passed \ through a BCS) LDA G \ It doesn't look like this code is ever reached, so CMP #240 \ presumably it's left over from development BCS rotm3
Name: GetRotationMatrix (Part 2 of 5) [Show more] Type: Subroutine Category: Maths (Geometry) Summary: Calculate sin(H G) for smaller angles Deep dive: The core driving model Trigonometry
Context: See this subroutine on its own page References: No direct references to this subroutine in this source file
.rotm2 \ If we get here then (U G) = yawRadians / 2 and U < 122 LDA #171 \ Set A = 171 JSR Multiply8x8 \ Set (A T) = (A * U) * U JSR Multiply8x8 \ = A * U^2 \ = 171 * (yawRadians / 2)^2 STA V \ Set (V T) = (A T) \ = 171 * (yawRadians / 2)^2 JSR Multiply8x16 \ Set (U T) = U * (V T) / 256 \ = (171 / 256) * (yawRadians / 2)^3 \ = 2/3 * (yawRadians / 2)^3 LDA G \ Set (A T) = (H G) - (U T) SEC \ = yawRadians / 2 - 2/3 * (yawRadians / 2)^3 SBC T \ STA T \ starting with the low bytes LDA H \ And then the high bytes SBC U ASL T \ Set (A T) = (A T) * 2 ROL A \ So we now have the following in (A T): \ \ (yawRadians / 2 - 2/3 * (yawRadians / 2)^3) * 2 \ \ = yawRadians - 4/3 * (yawRadians / 2)^3 \ \ = yawRadians - 4/3 * yawRadians^3 / 2^3 \ \ = yawRadians - 8/6 * yawRadians^3 * 1/8 \ \ = yawRadians - 1/6 * yawRadians^3 \ \ = yawRadians - yawRadians^3 / 3! \ \ The Taylor series expansion of sin(x) starts like \ this: \ \ sin(x) = x - (x^3 / 3!) + (x^5 / 5!) - ... \ \ If we take the first two parts of the series and \ apply them to yawRadians, we get: \ \ sin(yawRadians) = yawRadians - (yawRadians^3 / 3!) \ \ which is the same as our value in (A T) \ \ So the value in (A T) is equal to the first two parts \ of the Taylor series, and we have effectively just \ calculated an approximation of this: \ \ (A T) = sin(yawRadians) STA sinYawAngleHi,X \ Set (sinYawAngleHi sinYawAngleLo) = (A T) LDA T \ AND #%11111110 \ with the sign bit cleared in bit 0 of sinYawAngleLo to STA sinYawAngleLo,X \ denote a positive result JMP rotm5 \ Jump to rotm5 to move on to the next axis
Name: GetRotationMatrix (Part 3 of 5) [Show more] Type: Subroutine Category: Maths (Geometry) Summary: Calculate sin(H G) for bigger angles Deep dive: The core driving model Trigonometry
Context: See this subroutine on its own page References: No direct references to this subroutine in this source file
.rotm3 \ If we get here then (H G) = yawRadians / 2 and \ H >= 122 \ PI is represented by 804, as 804 / 256 = 3.14, so 201 \ represents PI/4, which we use in the following \ subtraction LDA #0 \ Set (U T) = (201 0) - (H G) SEC \ = PI/4 - yawRadians / 2 SBC G \ STA T \ starting with the low bytes LDA #201 \ And then the high bytes SBC H STA U STA V \ Set (V T) = (U T) \ = PI/4 - yawRadians / 2 JSR Multiply8x16 \ Set (U T) = U * (V T) / 256 \ = U * (PI/4 - yawRadians / 2) \ \ U is the high byte of (U T), which also contains \ PI/4 - yawRadians / 2, so this approximation holds \ true: \ \ (U T) = U * (PI/4 - yawRadians / 2) \ =~ (PI/4 - yawRadians / 2) ^ 2 ASL T \ Set (U T) = (U T) * 2 ROL U \ = (PI/4 - yawRadians / 2) ^ 2 * 2 \ By this point we have the following: \ \ (U T) = (PI/4 - yawRadians / 2) ^ 2 * 2 \ = ((PI/2 - yawRadians) / 2) ^ 2 * 2 \ \ If we define x = PI/2 - yawRadians, then we have: \ \ (U T) = (x / 2) ^ 2 * 2 \ = ((x ^ 2) / (2 ^ 2)) * 2 \ = (x ^ 2) / 2 \ \ The small angle approximation states that for small \ values of x, the following approximation holds true: \ \ cos(x) =~ 1 - (x ^ 2) / 2! \ \ As yawRadians is large, this means x is small, so we \ can use this approximation \ \ We are storing the cosine, which is in the range 0 to \ 1, in the 16-bit variable (U T), so in terms of 16-bit \ arithmetic, the 1 in the above equation is (1 0 0) \ \ So this is the same as: \ \ cos(x) =~ (1 0 0) - (x ^ 2) / 2! \ = (1 0 0) - (U T) \ \ It's a trigonometric identity that: \ \ cos(PI/2 - x) = sin(x) \ \ so we have: \ \ cos(x) = cos(PI/2 - yawRadians) \ = sin(yawRadians) \ \ and we already calculated that: \ \ cos(x) =~ (1 0 0) - (U T) \ \ so that means that: \ \ sin(yawRadians) = cos(x) \ =~ (1 0 0) - (U T) \ \ So we just need to calculate (1 0 0) - (U T) to get \ our result LDA #0 \ Set A = (1 0 0) - (U T) SEC \ SBC T \ starting with the low bytes AND #%11111110 \ Which we store in sinYawAngleLo, with bit 0 cleared to STA sinYawAngleLo,X \ denote a positive result (as it's a sign-magnitude \ number we want to store) LDA #0 \ And then the high bytes SBC U BCC rotm4 \ We now need to subtract the top bytes, i.e. the 1 in \ (1 0 0) and the 0 in (0 U T), while including the \ carry from the high byte subtraction \ \ So the top byte should be: \ \ A = 1 - 0 - (1 - C) \ = 1 - (1 - C) \ = C \ \ If the C flag is clear, then that means the top byte \ is zero, so we already have a valid result from the \ high and low bytes, so we jump to rotm4 to store the \ high byte of the result in sinYawAngleHi \ \ If the C flag is set, then the result is (1 A T), but \ the highest possible value for sin or cos is 1, so \ that's what we return \ \ Because sinYawAngle is a sign-magnitude number with \ the sign bit in bit 0, we return the following value \ to represent the closest value to 1 that we can fit \ into 16 bits: \ \ (11111111 11111110) LDA #%11111110 \ Set sinYawAngleLo to the highest possible positive STA sinYawAngleLo,X \ value (i.e. all ones except for the sign in bit 0) LDA #%11111111 \ Set A to the highest possible value of sinYawAngleHi, \ so we can store it in the next instruction .rotm4 STA sinYawAngleHi,X \ Store A in the high byte in sinYawAngleHi
Name: GetRotationMatrix (Part 4 of 5) [Show more] Type: Subroutine Category: Maths (Geometry) Summary: Loop back to calculate cos instead of sin Deep dive: The core driving model Trigonometry
Context: See this subroutine on its own page References: No direct references to this subroutine in this source file
.rotm5 CPX secondAxis \ If we just processed the second axis, then we have BEQ rotm6 \ now set both sinYawAngle and cosYawAngle, so jump to \ rotm6 to set their signs LDX secondAxis \ Otherwise set X = secondAxis so the next time we reach \ the end of the loop, we take the BEQ branch we just \ passed through LDA #0 \ Set (H G) = (201 0) - (H G) SEC \ SBC G \ starting with the low bytes STA G LDA #201 \ And then the high bytes SBC H STA H STA U \ Set (U G) = (H G) \ \ (U G) and (H G) were set to yawRadians / 2 for the \ first pass through the loop above, so we now have the \ following: \ \ 201 - yawRadians / 2 \ \ PI is represented by 804, as 804 / 256 = 3.14, so 201 \ represents PI/4, so this the same as: \ \ PI/4 - yawRadians / 2 \ \ Given that we expect (U G) to contain half the angle \ we are projecting, this means we are going to find the \ sine of this angle when we jump back to rotm1: \ \ PI/2 - yawRadians \ \ It's a trigonometric identity that: \ \ sin(PI/2 - x) = cos(x) \ \ so jumping back will, in fact, find the cosine of the \ angle JMP rotm1 \ Loop back to set the other variable of sinYawAngle and \ cosYawAngle to the cosine of the angle
Name: GetRotationMatrix (Part 5 of 5) [Show more] Type: Subroutine Category: Maths (Geometry) Summary: Apply the correct signs to the result Deep dive: The core driving model
Context: See this subroutine on its own page References: No direct references to this subroutine in this source file
.rotm6 \ By this point, we have the yaw angle vector's \ x-coordinate in sinYawAngle and the y-coordinate in \ cosYawAngle \ \ The above calculations were done on an angle that was \ reduced to a quarter-circle, so now we need to add the \ correct signs according to which quarter-circle the \ original playerYawAngle in (J T) was in LDA J \ If J is positive then playerYawAngle is positive (as BPL rotm7 \ J contains playerYawAngleHi), so jump to rotm7 to skip \ the following \ If we get here then playerYawAngle is negative \ \ The degree system in Revs looks like this: \ \ 0 \ -32 | +32 Overhead view of car \ \ | / \ \ | / 0 = looking straight ahead \ \|/ +64 = looking sharp right \ -64 -----+----- +64 -64 = looking sharp left \ /|\ \ / | \ \ / | \ \ -96 | +96 \ 128 \ \ So playerYawAngle is in the left half of the above \ diagram, where the x-coordinates are negative, so we \ need to negate the x-coordinate LDA #1 \ Negate sinYawAngle by setting bit 0 of the low byte, ORA sinYawAngleLo \ as sinYawAngle is a sign-magnitude number STA sinYawAngleLo .rotm7 LDA J \ If bits 6 and 7 of J are the same (i.e. their EOR is ASL A \ zero), jump to rotm8 to return from the subroutine as EOR J \ the sign of cosYawAngle is correct BPL rotm8 \ Bits 6 and 7 of J, i.e. of playerYawAngleHi, are \ different, so the angle is in one of these ranges: \ \ * 64 to 127 (%01000000 to %01111111) \ \ * -128 to -65 (%10000000 to %10111111) \ \ The degree system in Revs looks like this: \ \ 0 \ -32 | +32 Overhead view of car \ \ | / \ \ | / 0 = looking straight ahead \ \|/ +64 = looking sharp right \ -64 -----+----- +64 -64 = looking sharp left \ /|\ \ / | \ \ / | \ \ -96 | +96 \ 128 \ \ So playerYawAngle is in the bottom half of the above \ diagram, where the y-coordinates are negative, so we \ need to negate the y-coordinate LDA #1 \ Negate cosYawAngle by setting bit 0 of the low byte, ORA cosYawAngleLo \ as cosYawAngle is a sign-magnitude number STA cosYawAngleLo .rotm8 RTS \ Return from the subroutine
Name: GetAngleInRadians [Show more] Type: Subroutine Category: Maths (Geometry) Summary: Convert a 16-bit angle into radians, restricted to a quarter circle Deep dive: Trigonometry
Context: See this subroutine on its own page References: This subroutine is called as follows: * GetRotationMatrix (Part 1 of 5) calls GetAngleInRadians

Arguments: (A T) A yaw angle in Revs format (-128 to +127)
Returns: (U A) The angle, reduced to a quarter circle, converted to radians, and halved
.GetAngleInRadians ASL T \ Set (V T) = (A T) << 2 ROL A \ ASL T \ This shift multiplies (A T) by four, removing bits 6 ROL A \ and 7 in the process STA V \ \ The degree system in Revs looks like this: \ \ 0 \ -32 | +32 Overhead view of car \ \ | / \ \ | / 0 = looking straight ahead \ \|/ +64 = looking sharp right \ -64 -----+----- +64 -64 = looking sharp left \ /|\ \ / | \ \ / | \ \ -96 | +96 \ 128 \ \ The top byte of (A T) is in this range, so shifting to \ the left by two places drops bits 6 and 7 and scales \ the angle into the range 0 to 252, as follows: \ \ * 0 to 63 (%00000000 to %00111111) \ -> 0 to 252 (%00000000 to %11111100) \ \ * 64 to 127 (%01000000 to %01111111) \ -> 0 to 252 (%00000000 to %11111100) \ \ * -1 to -64 (%11111111 to %11000000) \ -> 252 to 0 (%11111100 to %00000000) \ \ * -65 to -128 (%10111111 to %10000000) \ -> 252 to 0 (%11111100 to %00000000) \ We now convert this number from a Revs angle into \ radians \ \ The value of (V T) represents a quarter-circle, which \ is PI/2 radians, but we actually multiply by PI/4 to \ return the angle in radians divided by 2, to prevent \ overflow in the GetRotationMatrix routine LDA #201 \ Set U = 201 STA U \ Fall through into Multiply8x16 to calculate: \ \ (U A) = U * (V T) / 256 \ = 201 * (V T) / 256 \ = (201 / 256) * (V T) \ = (3.14 / 4) * (V T) \ \ So we return (U A) = PI/4 * (V T) \ \ which is the original angle, reduced to a quarter \ circle, converted to radians, and halved
Name: Multiply8x16 [Show more] Type: Subroutine Category: Maths (Arithmetic) Summary: Multiply an 8-bit and a 16-bit number
Context: See this subroutine on its own page References: This subroutine is called as follows: * ApplyWingBalance calls Multiply8x16 * AssistSteering calls Multiply8x16 * GetRotationMatrix (Part 2 of 5) calls Multiply8x16 * GetRotationMatrix (Part 3 of 5) calls Multiply8x16 * Multiply8x16Signed calls Multiply8x16

Do the following multiplication of two unsigned numbers: (U T) = U * (V T) / 256 The result is also available in (U A).
.Multiply8x16 JSR Multiply8x8+2 \ Set (A T) = T * U STA W \ Set (W T) = (A T) \ = T * U \ \ So W = T * U / 256 LDA V \ Set A = V JSR Multiply8x8 \ Set (A T) = A * U \ = V * U STA U \ Set (U T) = (A T) \ = V * U LDA W \ Set (U T) = (U T) + W CLC \ ADC T \ starting with the low bytes STA T BCC mult1 \ And then the high bytes, so we get the following: INC U \ \ (U T) = (U T) + W \ = V * U + (T * U / 256) \ = U * (V + T / 256) \ = U * (256 * V + T) / 256 \ = U * (V T) / 256 \ \ which is what we want .mult1 RTS \ Return from the subroutine
Name: Multiply16x16 [Show more] Type: Subroutine Category: Maths (Arithmetic) Summary: Multiply a sign-magnitude 16-bit number and a signed 16-bit number
Context: See this subroutine on its own page References: This subroutine is called as follows: * MultiplyCoords calls Multiply16x16

This routine calculates: (A T) = (QQ PP) * (SS RR) / 256^2 It uses the following algorithm: (QQ PP) * (SS RR) = (QQ << 8 + PP) * (SS << 8 + RR) = (QQ << 8 * SS << 8) + (QQ << 8 * RR) + (PP * SS << 8) + (PP * RR) = (QQ * SS) << 16 + (QQ * RR) << 8 + (PP * SS) << 8 + (PP * RR) Finally, it replaces the low byte multiplication in (PP * RR) with 128, as an estimate, as it's a pain to multiply the low bytes of a signed integer with a sign-magnitude number. So the final result that is returned in (A T) is as follows: (A T) = (QQ PP) * (SS RR) / 256^2 = ((QQ * SS) << 16 + (QQ * RR) << 8 + (PP * SS) << 8 + 128) / 256^2 which is the algorithm that is implemented in this routine.
Arguments: (QQ PP) 16-bit signed integer (SS RR) 16-bit sign-magnitude integer with the sign bit in bit 0 of RR H The sign to apply to the result (in bit 7)
Returns: (A T) (QQ PP) * (SS RR) * abs(H)
.Multiply16x16 LDA QQ \ If (QQ PP) is positive, jump to muls1 to skip the BPL muls1 \ following LDA #0 \ (QQ PP) is negative, so we now negate (QQ PP) so it's SEC \ positive, starting with the low bytes SBC PP STA PP LDA #0 \ And then the high bytes SBC QQ \ STA QQ \ So we now have (QQ PP) = |QQ PP| LDA H \ Flip bit 7 of H, so when we set the result to the sign EOR #%10000000 \ of H below, this ensures the result is the correct STA H \ sign .muls1 LDA RR \ If bit 0 of RR is clear, then (SS RR) is positive, so AND #1 \ jump to muls2 BEQ muls2 LDA H \ Flip bit 7 of H, so when we set the result to the sign EOR #%10000000 \ of H below, this ensures the result is the correct STA H \ sign .muls2 LDA QQ \ Set U = QQ STA U LDA RR \ Set A = RR JSR Multiply8x8 \ Set (A T) = A * U \ = RR * QQ STA W \ Set (W T) = (A T) \ = RR * QQ LDA T \ Set (W V) = (A T) + 128 CLC \ = RR * QQ + 128 ADC #128 \ STA V \ starting with the low bytes BCC muls3 \ And then the high byte INC W \ So we now have (W V) = RR * QQ + 128 .muls3 LDA SS \ Set A = SS JSR Multiply8x8 \ Set (A T) = A * U \ = SS * QQ STA G \ Set (G T) = (A T) \ = SS * QQ LDA T \ Set (G W V) = (G T 0) + (W V) CLC \ ADC W \ starting with the middle bytes (as the low bytes are STA W \ simply V = 0 + V with no carry) BCC muls4 \ And then the high byte INC G \ So now we have: \ \ (G W V) = (G T 0) + (W V) \ = (SS * QQ << 8) + RR * QQ + 128 .muls4 LDA PP \ Set U = PP STA U LDA SS \ Set A = SS JSR Multiply8x8 \ Set (A T) = A * U \ = SS * PP STA U \ Set (U T) = (A T) \ = SS * PP LDA T \ Set (G T ?) = (G W V) + (U T) CLC \ ADC V \ starting with the low bytes (which we throw away) LDA U \ And then the high bytes ADC W STA T BCC muls5 \ And then the high byte INC G \ So now we have: \ \ (G T ?) = (G W V) + (U T) \ = (SS * QQ << 8) + RR * QQ + 128 + SS * PP \ = (QQ * SS) << 8 + (QQ * RR) + (PP * SS) \ + 128 \ = (QQ PP) * (SS RR) / 256 \ \ So: \ \ (G T) = (G T ?) / 256 \ = (QQ PP) * (SS RR) / 256^2 \ \ which is the result that we want .muls5 LDA G \ Set (A T) = (G T) BIT H \ We are about to fall through into Absolute16Bit, so \ this ensures we set the sign of (A T) to the sign in \ H, so we get: \ \ (A T) = (A T) * abs(H)
Name: Absolute16Bit [Show more] Type: Subroutine Category: Maths (Arithmetic) Summary: Calculate the absolute value (modulus) of a 16-bit number
This routine sets (A T) = |A T|. It can also return (A T) * abs(n), where A is given the sign of n.
Arguments: (A T) The number to make positive N flag Controls the sign to be applied: * If we want to calculate |A T|, do an LDA or equivalent before calling the routine * If we want to calculate (A T) * abs(n), do a BIT n before calling the routine * If we want to set the sign of (A T), then call with: * N flag clear to calculate (A T) * 1 * N flag set to calculate (A T) * -1
.Absolute16Bit BPL ScanKeyboard-1 \ If the high byte in A is already positive, return from \ the subroutine (as ScanKeyboard-1 contains an RTS) \ Otherwise fall through into Negate16Bit to negate the \ number in (A T), which will make it positive, so this \ sets (A T) = |A T|
Name: Negate16Bit [Show more] Type: Subroutine Category: Maths (Arithmetic) Summary: Negate a 16-bit number
Context: See this subroutine on its own page References: This subroutine is called as follows: * ApplyTyreForces calls Negate16Bit * ProcessDrivingKeys (Part 2 of 6) calls Negate16Bit * AssistSteering calls via Negate16Bit+2 * ProcessDrivingKeys (Part 2 of 6) calls via Negate16Bit+2 * SubtractCoords calls via Negate16Bit+2

This routine negates the 16-bit number (A T).
Other entry points: Negate16Bit+2 Set (A T) = -(U T)
.Negate16Bit STA U \ Set (U T) = (A T) LDA #0 \ Set (A T) = 0 - (U T) SEC \ = -(A T) SBC T \ STA T \ starting with the low bytes LDA #0 \ And then the high bytes SBC U RTS \ Return from the subroutine
Name: ScanKeyboard [Show more] Type: Subroutine Category: Keyboard Summary: Scan the keyboard for a specific key press
Arguments: X The negative inkey value of the key to scan for (in the range &80 to &FF)
Returns: Z flag The result: * Set if the key in X is being pressed, in which case BEQ will branch * Clear if the key in X is not being pressed, in which case BNE will branch
Other entry points: ScanKeyboard-1 Contains an RTS
.ScanKeyboard LDA #129 \ Call OSBYTE with A = 129, Y = &FF and the inkey value LDY #&FF \ in X, to scan the keyboard for key X JSR OSBYTE CPX #&FF \ If the key in X is being pressed, the above call sets \ both X and Y to &FF, so this sets the Z flag depending \ on whether the key is being pressed (so a BEQ after \ the call will branch if the key in X is being pressed) RTS \ Return from the subroutine
Name: FlushSoundBuffer [Show more] Type: Subroutine Category: Sound Summary: Flush the specified sound buffer Deep dive: The engine sounds
Context: See this subroutine on its own page References: This subroutine is called as follows: * ApplyTyresAndSkids calls FlushSoundBuffer * FlushSoundBuffers calls FlushSoundBuffer * MakeDrivingSounds calls FlushSoundBuffer

This routine flushes the specified sound channel buffer, but only if that channel's soundBuffer value is non-zero.
Arguments: X The number of the sound channel buffer to flush (0 to 3)
Returns: X X is unchanged A A is unchanged
.FlushSoundBuffer PHA \ Store the value of A on the stack so we can retrieve \ it before returning from the routine LDA soundBuffer,X \ If this buffer's soundBuffer value is zero, then there BEQ flus1 \ is nothing to flush, so jump to flus1 to return from \ the subroutine LDA #0 \ Set this buffer's soundBuffer value for this buffer to STA soundBuffer,X \ 0 to indicate that it has been flushed TXA \ Set bit 2 of X ORA #%00000100 \ TAX \ This changes X from the original range of 0 to 3, into \ the range 4 to 7, so it now matches the relevant sound \ buffer number (as buffers 4 to 7 are the buffers for \ sound channels 0 to 3) LDA #21 \ Call OSBYTE with A = 21 to flush buffer X, which JSR OSBYTE \ flushes the relevant sound channel buffer TXA \ Clear bit 2 of X, to reverse the X OR 4 above AND #%11111011 TAX .flus1 PLA \ Retrieve the value of A that we stored on the stack \ above, so it remains unchanged by the routine RTS \ Return from the subroutine
Name: MakeDrivingSounds [Show more] Type: Subroutine Category: Sound Summary: Make the relevant sounds for the engine and tyres Deep dive: The engine sounds
Context: See this subroutine on its own page References: This subroutine is called as follows: * MainDrivingLoop (Part 2 of 5) calls MakeDrivingSounds * MainDrivingLoop (Part 5 of 5) calls MakeDrivingSounds
.MakeDrivingSounds LDA tyreSqueal \ If bit 7 of tyreSqueal is clear for both tyres, jump ORA tyreSqueal+1 \ to soun1 to skip the following as no tyres are BPL soun1 \ squealing \ Otherwise we add some random pitch variation to the \ crash/contact sound and make the sound of the tyres \ squealing LDA VIA+&68 \ Read 6522 User VIA T1C-L timer 2 low-order counter \ (SHEILA &68), which decrements one million times a \ second and will therefore be pretty random CMP #63 \ If A < 63 (25% chance), jump to soun1 to skip the BCS soun1 \ following AND #3 \ Reduce A to a random number in the range 0 to 3 CLC \ Add 130 to A, so A is a random number in the range ADC #130 \ 130 to 133 STA soundData+28 \ Update byte #5 of sound #4 (low byte of pitch) so the \ pitch of the crash/contact sound wavers randomly LDA #3 \ Make sound #3 (tyre squeal) using envelope 1 LDY #1 JSR MakeSound .soun1 \ We now increment or decrement soundRevCount so it \ steps towards the value of soundRevTarget, which moves \ the pitch of the engine towards the current rev count LDX soundRevCount \ Set X = soundRevCount CPX soundRevTarget \ If X = soundRevTarget, jump to soun8 to return from BEQ soun8 \ the subroutine BCC soun2 \ If X < soundRevTarget, jump to soun2 to increment X DEX \ Decrement X and skip the next instruction (this BCS BCS soun3 \ is effectively a JMP as we passed through the BCC) .soun2 INX \ Increment X .soun3 STX soundRevCount \ Store X in soundRevCount, so soundRevCount moves one \ step closer to soundRevTarget \ We now do the following, depending on the updated \ value of soundRevCount in X: \ \ * If soundRevCount < 28, flush all the sound buffers \ (i.e. stop making any sounds) \ \ * If 28 <= soundRevCount < 92, make the engine \ exhaust sound, set the pitch of engine tone 1 to \ soundRevCount + 95 and the volume of engine tone 1 \ to 0, and make the sound of engine tone 1 \ \ * If soundRevCount >= 92, silence the exhaust, set \ the pitch of engine tone 1 to soundRevCount - 92, \ and make the sound of engine tone 1 CPX #28 \ If X < 28, then jump to soun9 to flush all the sound BCC soun9 \ buffers, as the rev count is too low for the engine to \ make a sound TXA \ Set A = X - 92 SEC SBC #92 BCS soun4 \ If the subtraction didn't underflow, i.e. X >= 92, \ then jump to soun4 to silence the engine exhaust and \ set the pitch of engine tone 1 to X - 92 PHA \ Store A on the stack to we can retrieve it after the \ following call LDA #0 \ Make sound #0 (engine exhaust) at the current volume JSR MakeSound-3 \ level PLA \ Retrieve the value of A that we stored on the stack, \ so A = X - 92 CLC \ Set A = A + 187 ADC #187 \ = X - 92 + 187 \ = X + 95 \ \ so we set the pitch of engine tone 1 to X + 95 LDY #0 \ Set Y = 0, so we set the volume of engine tone 1 to \ zero (silent) BEQ soun5 \ Jump to soun5 (this BEQ is effectively a JMP as Y is \ always zero) .soun4 LDX #0 \ Flush the buffer for sound channel 0, which will stop JSR FlushSoundBuffer \ the sound of the engine exhaust LDY volumeLevel \ Set Y to the current volume level .soun5 STA soundData+12 \ Update byte #5 of sound #1 (low byte of pitch), to set \ the pitch of engine tone 1 to A LDA #1 \ Make sound #1 (engine tone 1) with volume Y JSR MakeSound \ We now do the following, depending on the updated \ value of soundRevCount: \ \ * If the volume level is currently zero, make the \ sound of engine tone 2 sound with volume 0 \ \ * If soundRevCount >= 64, set the pitch of engine \ tone 2 to soundRevCount - 64, and make the sound \ of engine tone 2 \ \ * If soundRevCount < 64, make the sound of engine \ tone 2 with volume 0 LDY volumeLevel \ If the volume level is currently zero (no sound), jump BEQ soun7 \ to soun7 to make the engine tone 2 sound with volume 0 LDA soundRevCount \ Set A = soundRevCount - 64 SEC SBC #64 BCS soun6 \ If the subtraction didn't underflow, i.e. A >= 64, \ then jump to soun6 to set the pitch of engine tone 2 \ to soundRevCount - 64 LDY #0 \ Set Y = 0, so we set the volume of engine tone 2 to \ zero (silent) BEQ soun7 \ Jump to soun7 (this BEQ is effectively a JMP as Y is \ always zero) .soun6 STA soundData+20 \ Update byte #5 of sound #2 (low byte of pitch), to set \ the pitch of engine tone 2 to A .soun7 LDA #2 \ Make sound #2 (engine tone 2) with volume Y JSR MakeSound .soun8 RTS \ Return from the subroutine .soun9 JSR FlushSoundBuffers \ Flush all four sound channel buffers RTS \ Return from the subroutine
Name: ProcessShiftedKeys [Show more] Type: Subroutine Category: Keyboard Summary: Check for shifted keys (i.e. those that need SHIFT holding down to trigger) and process them accordingly
Context: See this subroutine on its own page References: This subroutine is called as follows: * FinishRace calls ProcessShiftedKeys * MainDrivingLoop (Part 5 of 5) calls ProcessShiftedKeys

Arguments: Y Scan for the first Y + 1 keys from shiftedKeys
.ProcessShiftedKeys STY T \ Set T to the number of keys to scan LDX #&FF \ Scan the keyboard to see if SHIFT is being pressed JSR ScanKeyboard BNE shif10 \ If SHIFT is not being pressed, jump to shif10 to \ return from the subroutine LDY T \ Set Y to the number of keys to scan, to use as a loop \ counter as we work our way backwards through the \ shiftedKeys table, from entry Y to entry 0 .shif1 STY T \ Set T to the loop counter LDX shiftedKeys,Y \ Fetch the next key number from the shiftedKeys table JSR ScanKeyboard \ Scan the keyboard to see if this key is being pressed BEQ shif2 \ If this key is being pressed, jump to shif2 to update \ the relevant configuration setting LDY T \ Otherwise set Y to the value of the loop counter DEY \ Decrement the loop counter to point to the next key in \ the table (working backwards) BPL shif1 \ Loop back to check the next key in the table until we \ have checked them all BMI shif3 \ None of the keys are being pressed, so jump to shif3 \ to skip updating the configuration bytes (this BMI is \ effectively a JMP as we just passed through a BPL) .shif2 \ If we get here then the Y-th key is being pressed, \ along with SHIFT, so we now update the relevant \ configuration byte, according to the settings in the \ configKeys table LDY T \ Otherwise set Y to the value of the loop counter, \ which gives us the offset of key that is being pressed \ within the shiftedKeys table LDA configKeys,Y \ Set X to the low nibble for this key's corresponding AND #&0F \ entry in the configKeys, which contains the offset of TAX \ the configuration byte from the first configuration \ byte at configStop LDA configKeys,Y \ Set A to the high nibble for this key's corresponding AND #&F0 \ entry in the configKeys, which contains the value that \ we need for the corresponding configuration byte STA configStop,X \ Set the corresponding configuration byte to the value \ in A .shif3 LDA configPause \ If configPause = 0, then neither COPY nor DELETE are BEQ shif6 \ being, so jump to shif6 \ If we get here then one of the pause buttons is being \ pressed BPL shif5 \ If bit 7 of configPause is clear, then this means bit \ 6 must be set, which only happens when the unpause key \ (DELETE) is being pressed, so jump to shif5 to unpause \ the game \ Otherwise we need to pause the game JSR FlushSoundBuffers \ Flush all four sound channel buffers to stop the sound \ while we are paused .shif4 JSR ResetTrackLines \ Reset the blocks at leftVergeStart, leftTrackStart, \ rightVergeStart, rightGrassStart and backgroundColour LDX #&A6 \ Scan the keyboard to see if DELETE is being pressed JSR ScanKeyboard BNE shif4 \ If DELETE is not being pressed, loop back to shif4 to \ remain paused, otherwise keep going to unpause the \ game .shif5 INC soundRevCount \ Increment soundRevCount to make the engine sound jump \ a little LDA #0 \ Set configPause = 0 to clear the pause/unpause key STA configPause \ press .shif6 LDY volumeLevel \ Set Y to the volume level, which uses the operating \ system's volume scale, with -15 being full volume and \ 0 being silent LDA mainLoopCounterLo \ If bit 0 of mainLoopCounterLo is set, which it will be AND #1 \ every other iteration round the main loop, jump to BNE shif9 \ shif9 to skip the following, so the sound changes more \ slowly than it would if we did this every loop LDA configVolume \ If configVolume = 0, jump to shif10 to return from the BEQ shif10 \ subroutine BPL shif7 \ If bit 7 of configVolume is clear, then this means bit \ 6 must be set, which only happens when the volume up \ (f5) key is being pressed, so jump to shif7 \ If we get here then we need to turn the volume down INY \ Increment Y to decrease the volume BEQ shif8 \ If Y is 0 or negative, then it is still a valid volume BMI shif8 \ level, so jump to shif8 to update the volume setting BPL shif9 \ Otherwise we have already turned the volume down as \ far as it will go, so jump to shif9 to clear the key \ press and return from the subroutine (this BPL is \ effectively a JMP as we just passed through a BMI) .shif7 \ If we get here then we need to turn the volume up DEY \ Decrement Y to increase the volume CPY #241 \ If Y < -15, then we have already turned the volume up BCC shif9 \ as far as it will go, so jump to shif9 to clear the \ key press and return from the subroutine \ Otherwise fall through into shif8 to update the \ volume setting .shif8 STY volumeLevel \ Store the updated volume level in volumeLevel TYA \ Set A = -Y, negated using two's complement EOR #&FF CLC ADC #1 ASL A \ Set envelopeData+12 = A << 3 ASL A \ = -Y * 8 ASL A \ STA envelopeData+12 \ which is 0 for no volume, or 120 for full volume, so \ this sets the target level for the end of the attack \ phase to a higher figure for higher volume settings LDA #0 \ Set up the envelope for the engine sound, with the JSR DefineEnvelope \ volume changed accordingly INC soundRevCount \ Increment soundRevCount to make the engine sound jump \ a little .shif9 LDA #0 \ Set configVolume = 0 to clear the volume key press STA configVolume .shif10 RTS \ Return from the subroutine
Name: SortDrivers [Show more] Type: Subroutine Category: Drivers Summary: Create a sorted list of driver numbers, ordered as specified
Context: See this subroutine on its own page References: This subroutine is called as follows: * MainLoop (Part 3 of 6) calls SortDrivers * MainLoop (Part 4 of 6) calls SortDrivers * MainLoop (Part 6 of 6) calls SortDrivers

This routine sorts the driver list in driversInOrder according to the value specified by argument A. It also populates carStatus with the position numbers for the sorted driver list, which will typically run from 0 to 19, but may also contain repeated numbers in the case of a tie. The routine uses a basic bubble sort algorithm, swapping neighbouring drivers repeatedly until the whole list is sorted. This is not very efficient, but as this is only done when showing the driver table between races, that doesn't matter.
Arguments: A Determines the order of the sorted list to create: * 0 = best lap times * Bit 6 set = accumulated points * Bit 7 set = total race times
Returns: positionNumber A list of position numbers, from 0 to 19, ready to print in the first column of the driver table (with 1 added), with drivers who are tied in the same position sharing the same number driversInOrder A list of driver numbers, sorted according to the value specified by argument A
.SortDrivers STA G \ Store A in G SED \ Set the D flag to switch arithmetic to Binary Coded \ Decimal (BCD) .sort1 LDX #0 \ Set V = 0, which we will use to indicate whether the STX V \ driversInOrder list is sorted \ \ We start at 0 to indicate it is sorted, and change it \ if we have to reorder the list STX positionNumber \ Set the first entry in positionNumber to 0, as the \ winning driver will always be in position 0 INX \ Set X = 1 as a position counter, counting through 1 to \ 19, which denotes the position number that we are \ processing in this iteration of the loop (we skip the \ first position as we already set it) .sort2 STX W \ Store the position counter in W LDY driversInOrder,X \ Set Y to the number of the driver at position X in the \ driversInOrder list ("this driver") TXA \ Set the X-th entry in positionNumber to the position STA positionNumber,X \ counter (as the X-th driver is in position X) LDA driversInOrder-1,X \ Set X to the number of the driver at position X - 1 in TAX \ the driversInOrder list ("the driver ahead") SEC \ Set the C flag for the subtractions below BIT G \ If bit 6 of G is set, jump to sort5 to compare total BVS sort5 \ points BMI sort6 \ If bit 7 of G is set, jump to sort6 to compare best \ lap times \ If we get here then bit 6 and 7 of G are clear, so we \ compare most recent lap times LDA bestLapTenths,Y \ Set (A H U) = this driver's best lap time SBC bestLapTenths,X \ - best lap time of the driver ahead STA U \ \ starting with the tenths of a second LDA bestLapSeconds,Y \ Then the seconds SBC bestLapSeconds,X STA H LDA bestLapMinutes,Y \ And then the minutes SBC bestLapMinutes,X BCC sort7 \ If the subtraction underflowed, then this driver's \ lap time is quicker than the lap time of the driver \ ahead, which is the wrong way round if we are trying \ to create a list where the winner has the fastest lap \ time, so jump to sort7 to swap them around in the \ driversInOrder list .sort3 ORA U \ At this point (A H U) contains the difference between ORA H \ the two drivers' times/points, so this jumps to sort4 BNE sort4 \ if any of the bytes in (A U H) are non-zero, i.e. if \ the two drivers have different times/points LDX W \ The two drivers have identical times/points, so set DEX \ we need to set the current driver's position number to LDA positionNumber,X \ be the same as the position number of the driver ahead STA positionNumber+1,X \ as there is a tie .sort4 \ If we get here then we move on to the next position LDX W \ Fetch the position counter that we stored in W above INX \ Increment the position counter to the next position CPX #20 \ Loop back until we have gone through the whole table BCC sort2 \ of 20 positions LDA V \ If V <> 0 then we had to alter the order of the BNE sort1 \ driversInOrder list, as it wasn't fully sorted, so \ we jump back to sort1 to repeat the whole process as \ we don't yet know that the list is fully sorted CLD \ Otherwise the driversInOrder list is sorted, so clear \ the D flag to switch arithmetic to normal JSR SetPlayerPositions \ Set the current player's position, plus the position \ ahead and the position behind RTS \ Return from the subroutine .sort5 LDA totalPointsLo,X \ Set (A H U) = total points of the driver ahead SBC totalPointsLo,Y \ - this driver's total points STA U \ \ starting with the low bytes LDA totalPointsHi,X \ Then the high bytes SBC totalPointsHi,Y STA H LDA totalPointsTop,X \ And then the top bytes SBC totalPointsTop,Y BCC sort7 \ If the subtraction underflowed, then this driver has \ more points than the driver ahead, which is the wrong \ way round if we are trying to create a list where the \ winner has the most points, so jump to sort7 to swap \ them around in the driversInOrder list BCS sort3 \ Jump to sort3 to check for a tie and move on to the \ next position (this BCS is effectively a JMP as we \ just passed through a BCC) .sort6 LDA totalRaceTenths,Y \ Set (A H U) = this driver's total race time SBC totalRaceTenths,X \ - total race time of the driver ahead STA U \ \ starting with the tenths of a second LDA totalRaceSeconds,Y \ Then the seconds SBC totalRaceSeconds,X STA H LDA totalRaceMinutes,Y \ And then the minutes SBC totalRaceMinutes,X BCS sort4 \ If the subtraction didn't underflow then the drivers \ are in the correct order, so jump to sort4 to move on \ to the next position \ Otherwise the subtraction underflowed, so this \ driver's total race time is quicker than the total \ race time of the driver ahead, which is the wrong way \ round if we are trying to create a list where the \ winner has the fastest time, so fall through into \ sort7 to swap them around in the driversInOrder list .sort7 \ If we get here then the two drivers we are comparing \ are in the wrong order in the driversInOrder list, so \ we need to swap them round \ \ At this point X contains the number of the driver \ ahead and Y contains the number of this driver STX T \ Store the number of the driver ahead in T LDX W \ Set X to the position counter TYA \ Set A to the number of this driver STA driversInOrder-1,X \ Set the number of the driver ahead (i.e. the position \ before the one we are processing) to A (i.e. the \ number of this driver) LDA T \ Set the number of this driver (i.e. the current STA driversInOrder,X \ position) to T (i.e. the number of the driver ahead) DEC V \ Decrement V so that is it non-zero, to indicate that \ we had to swap an entry in the driversInOrder list JMP sort4 \ Jump to sort4 to move on to the next position
Name: UpdateLapTimers [Show more] Type: Subroutine Category: Drivers Summary: Update the lap timers and display timer-related messages at the top of the screen
Context: See this subroutine on its own page References: This subroutine is called as follows: * MainDrivingLoop (Part 2 of 5) calls UpdateLapTimers
.UpdateLapTimers LDA raceStarted \ If bit 7 of raceStarted is clear then this is either BPL laps2 \ a practice or qualifying lap, so jump to laps2 to \ update the lap timers for qualifying \ If we get here then this is a race lap BIT updateDrivingInfo \ If bit 7 of updateDrivingInfo is clear then we do not BPL laps1 \ need to update the lap number, so jump to laps1 to \ skip straight to updating the driver positions LDA #%00000000 \ Clear bits 6 and 7 of updateDrivingInfo so we don't STA updateDrivingInfo \ update the number of laps again until the value of \ updateDrivingInfo changes to indicate that we should STA G \ Set G = 0 so the call to Print2DigitBCD below will \ print the second digit and will not print leading \ zeroes when printing the number of laps LDX currentPlayer \ Set X to the driver number of the current player LDA driverLapNumber,X \ Set A to the current lap number for the current player CMP #1 \ If A >= 1, set the C flag, otherwise clear it EOR #&FF \ Set A = numberOfLaps + ~A + C ADC numberOfLaps \ = numberOfLaps - A if A >= 1 \ = numberOfLaps - 1 if A = 0 PHP \ Store the resulting flags on the stack JSR ConvertNumberToBCD \ Convert the number in A into binary coded decimal \ (BCD), adding 1 in the process LDX #12 \ Print the number in A at column 12, pixel row 33, on LDY #33 \ the second text line at the top of the screen JSR Print2DigitBCD-6 PLP \ If the result of the above addition was positive, jump BPL laps1 \ to laps1 to skip printing the finished message LDX #53 \ Blank out the first text line at the top of the screen JSR PrintSecondLineGap \ and print token 53 on the second line, to give: \ \ " " \ " FINISHED " .laps1 LDA leaveTrackTimer \ If leaveTrackTimer is non-zero then the leave track BNE laps8 \ timer is counting down, so jump to laps8 to return \ from the subroutine without updating the text at the \ top of the screen JSR UpdatePositionInfo \ Otherwise update the position number and driver names \ at the top of the screen RTS \ Return from the subroutine .laps2 \ If we get here then this is a practice or qualifying \ lap LDX #1 \ Add time to the lap timer at (lapMinutes lapSeconds JSR AddTimeToTimer \ lapTenths), setting the C flag if the time has changed BIT updateDrivingInfo \ If bit 6 of updateDrivingInfo is set then we have BVS laps4 \ started the first lap, so jump to laps4 to skip the \ following and print the lap time only (we make the \ jump with the C flag indicating whether the timer has \ changed) BPL laps6 \ If bit 7 of updateDrivingInfo is clear then we do not \ need to update the lap time, so jump to laps6 to skip \ the following \ If we get here then we have started the first lap of \ practice or qualifying and we need to print the lap \ time LSR updateDrivingInfo \ Bit 7 of updateDrivingInfo is set and bit 6 is clear, \ so clear bit 7 and set bit 6 of updateDrivingInfo to \ indicate that we are now driving the first lap LDA #33 \ Set firstLapStarted = firstLapStarted + 33 CLC \ ADC firstLapStarted \ So if we have just started the first lap, then this STA firstLapStarted \ changes firstLapStarted from -33 to 0 BEQ laps3 \ If A = 0, then we just started the first qualifying or \ practice lap, so jump to laps3 to skip the following \ two instructions \ I am not sure if we ever get here, as the current lap \ time is never printed with tenths of a second LDA #%00100110 \ Print the current lap time at the top of the screen in JSR PrintLapTime+2 \ the following format: \ \ * %00 Minutes: No leading zeroes, print both digits \ * %10 Seconds: Leading zeroes, print both digits \ * %0 Tenths: Print tenths of a second \ * %11 Tenths: Leading zeroes, no second digit .laps3 LDX #1 \ Zero the lap timer JSR ZeroTimer BEQ laps6 \ Jump to laps6 (this BNE is effectively a JMP as the \ ZeroTimer routine sets the Z flag) .laps4 \ If we get here, then the C flag indicates whether the \ lap timer at (lapMinutes lapSeconds lapTenths) has \ changed LDA firstLapStarted \ If firstLapStarted = 0 then we are currently driving BEQ laps5 \ the first qualifying or practice lap, so jump to laps5 \ to print the current lap time, but not the best lap \ time (as we haven't completed a lap yet) \ \ We jump with the C flag indicating whether the timer \ has changed DEC firstLapStarted \ Decrement firstLapStarted BNE laps6 \ If firstLapStarted is non-zero, jump to laps6 to skip \ printing any lap times JSR PrintBestLapTime \ Print the best lap time and the current lap time at \ the top of the screen LDA #2 \ Print two spaces JSR PrintSpaces BEQ laps6 \ Jump to laps6 to skip the following (this BEQ is \ effectively a JMP, as PrintSpaces sets the Z flag) .laps5 \ If we get here, then the C flag indicates whether the \ lap timer at (lapMinutes lapSeconds lapTenths) has \ changed BCC laps6 \ If the C flag is clear then the timer has not changed, \ so jump to laps6 to skip the following instruction JSR PrintLapTime \ Print the current lap time at the top of the screen in \ the following format: \ \ * Minutes: No leading zeroes, print both digits \ * Seconds: Leading zeroes, print both digits \ * Tenths: Do not print tenths of a second .laps6 LDA qualifyingTime \ If bit 7 of qualifyingTime is set then this is a BMI laps8 \ practice lap (i.e. qualifyingTime = 255), so jump to \ laps8 to return from the subroutine \ If we get here then this is a qualifying lap, and the \ number of minutes of qualifying lap time is in A, as \ follows: \ \ * A = 4 indicates 5 minutes of qualifying time \ \ * A = 9 indicates 10 minutes of qualifying time \ \ * A = 25 indicates 26 minutes of qualifying time CMP clockMinutes \ If A < clockMinutes then we have reached the end of BCC laps7 \ qualifying time, so jump to laps7 to display the \ time-up message BNE laps8 \ If A <> clockMinutes, i.e. A > clockMinutes, then \ there is still some qualifying time left, so jump to \ laps8 to return from the subroutine BIT qualifyTimeEnding \ If bit 6 of qualifyTimeEnding is set, then we have BVS laps8 \ already displayed the one-minute warning, so jump to \ laps8 to return from the subroutine LDA #%01000000 \ Set bit 6 of qualifyTimeEnding to indicate that the STA qualifyTimeEnding \ one-minute warning has been displayed LDX #41 \ Print token 41 on the first text line at the top of JSR PrintFirstLine \ the screen, to give: \ \ " Less than one minute to go " RTS \ Return from the subroutine .laps7 LDA qualifyTimeEnding \ If bit 7 of qualifyTimeEnding is set, then we have BMI laps8 \ already displayed the time-up message, so jump to \ laps8 to return from the subroutine LDA #%11000000 \ Set bits of 6 and 7 of qualifyTimeEnding to indicate STA qualifyTimeEnding \ that we have displayed both the one-minute warning and \ the time-up message LDA #60 \ Set leaveTrackTimer = 60, so we leave the track in 60 STA leaveTrackTimer \ main loop iterations and return to the game menu LDX #42 \ Print token 42 on the first text line at the top of JSR PrintFirstLine \ the screen, to give: \ \ " YOUR TIME IS UP! " .laps8 RTS \ Return from the subroutine
Name: PlaceCarsOnTrack [Show more] Type: Subroutine Category: Car geometry Summary: Position the cars on the track, ready for a race or qualifying lap
Context: See this subroutine on its own page References: This subroutine is called as follows: * ResetVariables calls PlaceCarsOnTrack

This routine places the cars on the track, correctly spaced out and on alternating sides, and populates the track segment buffer so the first entry is 32 segments in front of the current player, and the whole buffer is full of buffered segments.
Arguments: A The distance between each car: * 1 if this is a race * The value of trackCarSpacing if this is practice or qualifying (40 for Silverstone)
.PlaceCarsOnTrack STA V \ Store the value of A in V, so V contains the size of \ the gap that we want to insert between the cars SEC \ Set bit 7 of updateLapTimes, so when we move the cars ROR updateLapTimes \ forward in MoveObjectForward, the lap number and lap \ time are not affected \ We start by incrementing the segment numbers for each \ car, to "move" the cars along the track, stopping when \ driver 0 is on segment 0 (i.e. when it has reached the \ starting line) \ \ Before the call to PlaceCarsOnTrack, we set each car's \ objectSegment to the value of trackStartLine, which is \ 843 for Silverstone \ \ The following loop moves all the cars forwards one \ segment at a time, until objectSegment wraps round to \ zero (which it does when it reaches the value of \ trackLength, which is 1024 for Silverstone, so the \ segment numbers go from 843 to 1023 and then to 0) \ \ In other words, the following moves the cars forward \ by 1024 - 843 segments from the start of section 0, \ or 181 segments, so the starting line at Silverstone \ is at segment 181, and this value is defined by the \ trackStartLine value in the track data file .rcar1 LDX #19 \ Set a loop counter in X to loop through the drivers .rcar2 JSR MoveObjectForward \ Move driver X forwards by one segment DEX \ Decrement the loop counter BPL rcar2 \ Loop back until we have processed all 20 drivers LDA objectSegmentLo \ If objectSegment for driver 0 is non-zero, jump back ORA objectSegmentHi \ to rcar1 to repeat the above loop BNE rcar1 \ The drivers are now all at the starting line (as they \ all started out at the same place before the above \ moves) LDA #&FF \ Set G = -1, so it can be incremented to 0 as the start STA G \ of the outer loop at rcar5 \ We now jump into a nested loop, with an inner loop \ between rcar3 and rcar5 an outer loop between rcar3 \ and rcar6 \ \ We iterate round the outer loop with G from 0 to 19, \ and for each outer iteration, we iterate round the \ inner loop with X from G to 19 \ \ The outer loop runs through each position, while the \ inner loop runs through each car behind that position, \ so the cars get moved backwards around the track as a \ group, with one car being dropped off after each inner \ loop in the correct position \ \ Specifically, the first iteration of the inner loop \ moves the cars in positions 0 to 19 backwards, then \ the cars in positions 1 to 19, then 2 to 19 and so on, \ leaving a trail of cars behind it as it works back \ along the track, so the car in position 0 is first, \ then the car in position 1, and so on to position 19 BNE rcar5 \ Jump to rcar5 (this BNE is effectively a JMP as A is \ never zero) .rcar3 \ This is the start of the inner loop, which runs \ through each position from G to 19, moving each car \ backwards V times (so each car moves backwards by the \ distance in V) LDA V \ Set W = V, so W contains the size of the gap that we STA W \ want to insert between the cars .rcar4 TXA \ Store X on the stack PHA LDA driversInOrder,X \ Set X to the number of the driver in position X TAX JSR MoveObjectBack \ Move driver X backwards along the track PLA \ Retrieve X from the stack TAX DEC W \ Decrement W BPL rcar4 \ Loop back until we have repeated the above W times INX \ Increment the loop counter CPX #20 \ Loop back until we have processed all 20 drivers BCC rcar3 .rcar5 \ This is where we join the loop with G = -1, so the \ following increments G to 0 as soon as we join \ \ This outer part of the loop runs through each position \ in G, from 0 to 19, and calls the inner loop above for \ each value of G INC G \ Increment G LDX G \ Set X = G, so the inner loop does G to 19 CPX #20 \ Loop back until we have done G = 0 to 19 BCC rcar3 \ At this point the cars are spaced out by the correct \ distance, working backwards from position 0 at the end \ of the track (i.e. just before the starting line), to \ position 19 at the back of the pack \ We now use the currently unused object 23 to work out \ the number of track segments we need to initialise in \ front of the current driver, by first moving forwards \ until we are exactly 32 segments in front of the \ current player, then moving backwards by 49 segments, \ and then moving backwards until we reach the start of \ the track section, leaving segmentCounter set to the \ total number of segments we have moved, starting from \ the start position of 32 segments in front of the \ current driver .rcar6 LDX #23 \ Set X to object 23 JSR MoveObjectForward \ Move object 23 forwards by one segment LDY #23 \ Set Y to object 23 LDX currentPlayer \ Set X to the driver number of the current player SEC \ Set the C flag for a 16-bit calculation in the call \ to CompareSegments JSR CompareSegments \ Set A and T to the distance between drivers X and Y BCS rcar6 \ If the C flag is set then the cars are far apart, so \ jump to rcar6 to keep moving object 23 forwards CMP #32 \ If A <> 32, jump to rcar6 to keep moving object 23 BNE rcar6 \ forwards \ At this point, object 23 is a distance of exactly 32 \ segments in front of the current player \ We now move object 23 back by 49 segments LDX #23 \ Set X to object 23 LDA #49 \ Set V = 49, to use as a loop counter from 49 to 1 STA V STA segmentCounter \ Set segmentCounter = 49 .rcar7 JSR MoveObjectBack \ Move object 23 backwards along the track DEC V \ Decrement the loop counter BNE rcar7 \ Loop back until we have moved object 23 backwards by \ 49 segments \ We now move object 23 backwards until it moves into a \ new track section, incrementing segmentCounter for \ each segment moved (which we set to 49 above, so it \ will keep tally of the total number of segments we \ have moved backwards) .rcar8 INC segmentCounter \ Increment segmentCounter JSR MoveObjectBack \ Move object 23 backwards along the track, setting the \ C flag if we move into a new track section BCC rcar8 \ Loop back to keep moving object 23 backwards until it \ moves into a new track section \ \ We don't care where object 23 has ended up, but we \ will use the value of segmentCounter below when \ populating the segment buffer \ We now move the cars to alternating sides of the track \ so the grid is staggered LDA #80 \ Set A to 80, which we will flip between 80 and 175 to \ alternate cars between the right (80) and left (175) \ side of the track, where 0 is full right and 255 is \ full left LDY #19 \ Set a loop counter in Y to loop through the positions .rcar9 LDX driversInOrder,Y \ Set X to the number of the driver in position X EOR #&FF \ Flip A between 80 and 175 STA carRacingLine,X \ Set the racing line of the car on the track (i.e. its \ left-right position) to the value in A DEY \ Decrement the loop counter to move on to the next \ position BPL rcar9 \ Loop back until we have staggered the whole pack LDA #0 \ Set frontSegmentIndex = 0, as the segment buffer is STA frontSegmentIndex \ currently empty (and we are about to fill it, which \ will update frontSegmentIndex accordingly) \ We now call GetTrackSegment segmentCounter times, \ where segmentCounter is the number of times we moved \ object 23 backwards in the above \ \ This populates the track segment buffer, making sure \ it is fully populated by working forwards from where \ object 23 ended up, one segment at a time, all the way \ to 32 segments in front of the current player, which \ is where we want the front segment of the track \ segment buffer to be .rcar10 JSR GetTrackSegment \ Initialise the next track segment in the track segment \ buffer, setting up the track segment indexes \ accordingly DEC segmentCounter \ Decrement the counter in segmentCounter BNE rcar10 \ Loop back until we have called GetTrackSegment \ segmentCounter times, by which time the buffer entry \ at frontSegmentIndex is 32 segments in front of the \ current player, the buffer entry at playerSegmentIndex \ is the current player, and all 40 entries in the \ buffer are populated with the correct segments \ \ So from this point onwards, object 23 is the object \ that corresponds to the front segment of the track \ segment buffer LSR updateLapTimes \ Clear bit 7 of updateLapTimes, so any further calls to \ MoveObjectForward will update the lap number and lap \ time once again RTS \ Return from the subroutine
Name: CheckForCrash [Show more] Type: Subroutine Category: Car geometry Summary: Check to see if we have crashed into the fence, and if so, display the fence and make the crash sound
Context: See this subroutine on its own page References: This subroutine is called as follows: * MainDrivingLoop (Part 2 of 5) calls CheckForCrash
.CheckForCrash LDA edgeDistanceHi \ If edgeDistanceHi < 2, then we are too close to the CMP #2 \ verge to have reached the fence or to spin out on the BCC cras3 \ grass, so jump to cras3 to return from the subroutine \ If we get here then we are quite far off the track LDA edgeYawAngle \ Set A to the yaw angle of the track segment that is \ closest to the player's car, from the point of view of \ the car JSR Absolute8Bit \ Set A = |A| \ = |edgeYawAngle| CMP #96 \ If A >= 96, then the nearest track segment to the BCS cras1 \ player is within a cone stretching out behind the car \ to 45 degrees on each side, so that's between -96 and \ +96 in the following diagram: \ \ 0 \ -32 | +32 Overhead view of car \ \ | / \ \ | / 0 = looking straight ahead \ \|/ +64 = looking sharp right \ -64 -----+----- +64 -64 = looking sharp left \ /|\ \ / | \ \ / | \ \ -96 | +96 \ 128 \ \ If the nearest track segment is behind us in this way, \ jump to cras1 to crash into the fence \ If we get here then we have not hit the fence but we \ are off the track, so we spin out LDA #20 \ Set A = 20 so we apply a yaw angle spin of magnitude \ 20 to the car BIT spinYawAngleTop \ Set the N flag according to the sign in bit 7 of \ spinYawAngleTop, so the call to Absolute8Bit sets the \ sign of A to the same sign as the spin yaw angle (so \ the car spins out in the same direction as it is \ currently spinning) JSR Absolute8Bit \ Set A = 20 * abs(spinYawAngle) JMP SquealTyres \ Jump to SquealTyres to update spinYawAngle and make \ the tyres squeal, returning from the subroutine using \ a tail call .cras1 DEC crashedIntoFence \ Decrement crashedIntoFence from 0 to &FF so the main \ driving loop will pause while showing the fence INC horizonLine \ Increment horizonLine to simulate us ditching forward \ into the fence (so the horizon goes up by a line) JSR DrawFence \ Draw the fence that we crash into when running off the \ track JSR FlushSoundBuffers \ Flush all four sound channel buffers LDA #4 \ Make sound #4 (crash/contact) at the current volume JSR MakeSound-3 \ level LDA #0 \ Set A = 0, so we can use it to reset variables to zero \ in the following loop LDX #30 \ We now zero all 30 variable bytes from xPlayerSpeedHi \ to xSteeringForceHi, so set up a loop counter in X .cras2 STA xPlayerSpeedHi,X \ Zero the X-th byte from xPlayerSpeedHi DEX \ Decrement the loop counter BPL cras2 \ Loop back until we have zeroed all variables from \ xPlayerSpeedHi to xSteeringForceHi STA engineStatus \ Set engineStatus = 0 to turn off the engine STA yGravityDelta \ Set yGravityDelta = 0 to cancel the effect of gravity \ on the car STA soundRevCount \ Set soundRevCount = 0 to stop the engine sound STA soundRevTarget \ Set soundRevTarget = 0 to stop the engine sound LDA #127 \ Set heightAboveTrack = 127 so the car is dropped from STA heightAboveTrack \ this height by the crane (if this is a Novice race, in \ which case it restarts without calling ResetVariables, \ which otherwise would zero heightAboveTrack) LDA #31 \ Set oddsOfEngineStart = 31 to make it harder to STA oddsOfEngineStart \ restart the engine (with a 1 in 32 chance) .cras3 RTS \ Return from the subroutine
Name: FinishRace [Show more] Type: Subroutine Category: Main loop Summary: Continue running the race until all the non-player drivers have finished and we have a result
Context: See this subroutine on its own page References: This subroutine is called as follows: * MainDrivingLoop (Part 4 of 5) calls FinishRace

This routine is beavering away in the background when the game displays the "PLEASE WAIT" message, after we finish qualifying or racing. It keeps running the simulation in the background until all the other drivers have finished their race or qualifying laps.
.FinishRace LDA #0 \ Set playerMoving = 0 to denote that the player's car STA playerMoving \ is stationary STA raceStarting \ Set raceStarting = 0 so the call to MoveCars below \ will move the cars round the track JSR HideAllCars \ Set all the cars to be hidden LDX currentPlayer \ Clear the current player's total race time if this is JSR ClearTotalRaceTime \ an incomplete race .fini1 JSR ProcessTime \ Increment the timers and the main loop counter, and \ set the speed for the next non-player driver LDY #0 \ Check for SHIFT and right arrow JSR ProcessShiftedKeys LDA configStop \ If bit 7 of configStop is set then we must be pressing BMI fini4 \ either SHIFT-f0 for a pit stop or SHIFT and right \ arrow to restart the game, so jump to fini4 to return \ from the subroutine \ If we get here then the race or qualifying lap has \ finished naturally, rather than being aborted, and \ this is not a pit stop \ \ So we now need to keep running the race or qualifying \ lap (albeit with everything hidden from the player) to \ get the final set of results for the whole pack JSR MoveCars \ Move the cars around the track, to keep the race going JSR ProcessOvertaking \ Process overtaking manoeuvres for the non-player \ drivers JSR SetPlayerPositions \ Set the current player's position, plus the position \ ahead and the position behind LDX #19 \ We now loop through the drivers, checking whether they \ have finished the race, so set a loop counter in X for \ the driver number LDA raceStarted \ If bit 7 of raceStarted is set then this is a race BMI fini2 \ rather than practice or qualifying, so jump to fini2 \ to work through the drivers and wait for them all to \ finish the race \ If we get here, then this is a qualifying lap CPX currentPlayer \ If the player is not driver 19, then jump to fini4 to BNE fini4 \ return from the subroutine, as there is more than one \ human player and this is not the first human to race, \ so we already have qualifying times for all the \ drivers \ If we get here, then this is a qualifying lap and the \ current player is driver 19, which means this is \ either the only human player, or the first human to \ race \ \ In any event, we need to make sure that we run the \ qualifying lap long enough to get lap times for all \ the other drivers, so we run the race for at least \ 14 * 256 main loop iterations \ \ This ensures we get lap times for all the drivers, \ even if the player decides to quit qualifying early LDA mainLoopCounterHi \ If the high byte main loop counter < 14, loop back to CMP #14 \ fini1 to keep running the race BCC fini1 RTS \ Return from the subroutine .fini2 \ If we get here, then this is a race rather than \ qualifying LDA objectStatus,X \ If bit 6 of driver X's objectStatus is set, then AND #%01000000 \ driver X has finished the race, so jump to fini3 BNE fini3 \ to check the next driver LDA numberOfLaps \ If numberOfLaps >= driver X's lap number, jump to CMP driverLapNumber,X \ fini1 to keep running the race, as driver X hasn't BCS fini1 \ finished the race yet .fini3 DEX \ Decrement the driver counter in X BPL fini2 \ Loop back until all the drivers have finished the race .fini4 RTS \ Return from the subroutine
Name: PushCarOffTrack [Show more] Type: Subroutine Category: Car geometry Summary: Push a car off the track and out of the race Deep dive: Tactics of the non-player drivers
Context: See this subroutine on its own page References: This subroutine is called as follows: * ProcessContact calls PushCarOffTrack

Arguments: X The driver number of the car to push off the track
.PushCarOffTrack CPX #20 \ If X >= 20 then this is not a valid driver, so return BCS BuildPlayerCar-1 \ from the subroutine (as BuildPlayerCar-1 contains an \ RTS) LDA carRacingLine,X \ Fetch the car's current racing line AND #%01111111 \ Clear bit 7 and set bits 0, 2 and 6 ORA #%01000101 STA carSteering,X \ Update the car's steering byte, so the car does the \ following: \ \ * Bit 7 clear = veer to the left \ \ * Bit 6 set = only apply this steering if there is \ room to do so, otherwise just keep the \ car on the track \ \ * Bits 0-5 = set steering amount to at least 5 \ (%101) LDA #%10010001 \ Set bits 0, 4 and 7 of the car's status byte, so: STA carStatus,X \ \ * Bit 0 set = do not update this carStatus byte when \ in the ProcessOvertaking routine, so \ overtaking tactics are disabled \ \ * Bit 4 set = do not follow the segment's steering \ line in segmentSteering, so the car \ doesn't automatically steer around \ corners \ \ * Bit 7 set = apply brakes \ Fall through into ClearTotalRaceTime to set the car \ object to be hidden and no longer racing, and reset \ the car's total race time
Name: ClearTotalRaceTime [Show more] Type: Subroutine Category: Drivers Summary: Clear a specified driver's total race time following the end of an incomplete race
Context: See this subroutine on its own page References: This subroutine is called as follows: * FinishRace calls ClearTotalRaceTime

Arguments: X The driver number
.ClearTotalRaceTime LDA numberOfLaps \ Compare numberOfLaps with the driver X's lap number CMP driverLapNumber,X LDA #%11000000 \ Set bits 6 and 7 of the status byte for driver X's car STA objectStatus,X \ object, so it's hidden (bit 7) and is no longer racing \ (bit 6) BCC clap1 \ If numberOfLaps < driver X's lap number, then the \ driver has finished the race, so skip the following \ instruction STA totalRaceMinutes,X \ The player didn't finish the race, so set the player's \ total race time to &C0 minutes, or 120 minutes .clap1 RTS \ Return from the subroutine
Name: BuildPlayerCar [Show more] Type: Subroutine Category: 3D objects Summary: Build the objects for the player's car
Context: See this subroutine on its own page References: This subroutine is called as follows: * MainDrivingLoop (Part 1 of 5) calls BuildPlayerCar * PushCarOffTrack calls via BuildPlayerCar-1

Returns: xPlayerCoord The 3D coordinates of the player's car playerYawAngle The yaw angle of the player's car around the y-axis
Other entry points: BuildPlayerCar-1 Contains an RTS
.BuildPlayerCar LDX currentPlayer \ Set X to the driver number of the current player STX thisDriver \ Set thisDriver to the driver number of the current \ player STX objectNumber \ Set objectNumber to the driver number of the current \ player LDY playerSegmentIndex \ Build the car object for the current player using JSR BuildCarObjects \ the player's track segment from the track segment \ buffer, returning the car's 3D coordinates in xCoord2 LDX #2 \ We are about to copy the three axes of the resulting \ vectors, so set an axis counter in X .bpla1 LDA xCoord2Lo,X \ Copy the car's 3D coordinates from xCoord2 into STA xPlayerCoordHi,X \ xPlayerCoord LDA xCoord2Hi,X STA xPlayerCoordTop,X DEX \ Decrement the axis counter BPL bpla1 \ Loop back until we have copied all three axes LDA playerSegmentIndex \ Set A = playerSegmentIndex + 3 CLC \ ADC #3 \ to move on to the next track segment CMP #120 \ If A < 120, then we haven't reached the end of the BCC bpla2 \ track segment buffer, so jump to bpla2 to store the \ updated value LDA #0 \ We just reached the end of the track segment buffer, \ so set A = 0 to wrap round to the start .bpla2 TAY \ Set Y to the updated track segment index * 3 LDX thisDriver \ Set X to the driver number of the current player JSR BuildCarObjects \ Build the car object for the current player using the \ updated track segment, returning the object's yaw \ angle in objYawAngle LDA objYawAngleLo,X \ Copy the low byte of the yaw angle to playerYawAngleLo STA playerYawAngleLo LDA objYawAngleHi,X \ Copy the high byte of the yaw angle to EOR directionFacing \ playerYawAngleHi, flipping the sign of the STA playerYawAngleHi \ coordinate if we are facing backwards along the track RTS \ Return from the subroutine
Name: GetSectionCoord [Show more] Type: Subroutine Category: Track geometry Summary: Copy a three-part 16-bit coordinate from the track section data Deep dive: Building a 3D track from sections and segments
Context: See this subroutine on its own page References: This subroutine is called as follows: * BuildRoadSign calls GetSectionCoord * GetSectionAngles (Part 3 of 3) calls GetSectionCoord * GetSectionCoords calls GetSectionCoord

This routine is normally called with X as a multiple of 3 in the range 0 to 117, representing track segments 0 to 39. The routine copies the following section coordinates from the track section data for section Y * 8: * xTrackSectionI * yTrackSectionI * zTrackSectionI and stores them in the X-th coordinate in (xSegmentCoordI, ySegmentCoordI, zSegmentCoordI). Specifically, this copies data from the Y-th track section entry: (xTrackSectionIHi xTrackSectionILo) (yTrackSectionIHi yTrackSectionILo) (zTrackSectionIHi zTrackSectionILo) and stores it in the X-th track segment: (xSegmentCoordIHi xSegmentCoordILo) (ySegmentCoordIHi ySegmentCoordILo) (zSegmentCoordIHi zSegmentCoordILo) This routine is also called with X = &FD, in which case it copies the following: Y-th (xTrackSectionIHi xTrackSectionILo) to (xCoord2Hi xCoord2Lo) Y-th (yTrackSectionIHi yTrackSectionILo) to (yCoord2Hi yCoord2Lo) Y-th (zTrackSectionIHi zTrackSectionILo) to (zCoord2Hi zCoord2Lo)
Arguments: Y The number of the track section * 8 whose coordinates we want to fetch X The place to store the data: * 0-117 = The index * 3 of the track segment where we store the coordinates * &FD = Copy to (xCoord2, yCoord2, zCoord2)
.GetSectionCoord LDA xTrackSectionILo,Y \ Copy the following 16-bit coordinates: STA xSegmentCoordILo,X \ LDA yTrackSectionILo,Y \ * The Y-th xTrackSectionI to the X-th xSegmentCoordI STA ySegmentCoordILo,X \ LDA zTrackSectionILo,Y \ * The Y-th yTrackSectionI to the X-th ySegmentCoordI STA zSegmentCoordILo,X \ \ * The Y-th zTrackSectionI to the X-th zSegmentCoordI \ \ starting with the low bytes LDA xTrackSectionIHi,Y \ And then the high bytes STA xSegmentCoordIHi,X LDA yTrackSectionIHi,Y STA ySegmentCoordIHi,X LDA zTrackSectionIHi,Y STA zSegmentCoordIHi,X RTS \ Return from the subroutine
Name: GetSectionCoords [Show more] Type: Subroutine Category: Track geometry Summary: Copy two three-part 16-bit coordinates from the track section data Deep dive: Building a 3D track from sections and segments
Context: See this subroutine on its own page References: This subroutine is called as follows: * GetFirstSegment calls GetSectionCoords

This routine is normally called with X as a multiple of 3 in the range 0 to 117, representing track segments 0 to 39. The routine copies the following track section coordinates from the track section data for section Y * 8: * xTrackSectionI * yTrackSectionI * zTrackSectionI and stores them in the X-th coordinate in (xSegmentCoordI, ySegmentCoordI, zSegmentCoordI). It also copies the following track section coordinates: * xTrackSectionO * yTrackSectionI * zTrackSectionO and stores them in the X-th coordinate in (xSegmentCoordO, ySegmentCoordO, zSegmentCoordO). Note that the y-coordinate is set to the same value as the y-coordinate from the first copy. It also sets thisVectorNumber to trackSectionFrom for the Y-th track section data. Specifically, this copies data from the Y-th track section entry: (xTrackSectionIHi xTrackSectionILo) (yTrackSectionIHi yTrackSectionILo) (zTrackSectionIHi zTrackSectionILo) (xTrackSectionOHi xTrackSectionOLo) (yTrackSectionIHi yTrackSectionILo) (zTrackSectionOHi zTrackSectionOLo) trackSectionFrom and stores it in the X-th track segment: (xSegmentCoordIHi xSegmentCoordILo) (ySegmentCoordIHi ySegmentCoordILo) (zSegmentCoordIHi zSegmentCoordILo) (xSegmentCoordOHi xSegmentCoordOLo) (ySegmentCoordOHi ySegmentCoordOLo) (zSegmentCoordOHi zSegmentCoordOLo) thisVectorNumber
Arguments: Y The number of the track section * 8 whose coordinates we want to fetch X The index * 3 of the track segment where we store the coordinates
.GetSectionCoords JSR GetSectionCoord \ Copy the following 16-bit coordinate: \ \ * The Y-th xTrackSectionI to the X-th xSegmentCoordI \ \ * The Y-th yTrackSectionI to the X-th ySegmentCoordI \ \ * The Y-th zTrackSectionI to the X-th zSegmentCoordI LDA xTrackSectionOLo,Y \ Copy the following 16-bit coordinate: STA xSegmentCoordOLo,X \ LDA zTrackSectionOLo,Y \ * The Y-th xTrackSectionO to the X-th xSegmentCoordO STA zSegmentCoordOLo,X \ \ * The Y-th zTrackSectionO to the X-th zSegmentCoordO \ \ starting with the low bytes LDA xTrackSectionOHi,Y \ And then the high bytes STA xSegmentCoordOHi,X LDA zTrackSectionOHi,Y STA zSegmentCoordOHi,X LDA trackSectionFrom,Y \ Set thisVectorNumber = the Y-th trackSectionFrom STA thisVectorNumber \ Fall through into CopySectionData to copy the \ following 16-bit coordinate: \ \ * The Y-th yTrackSectionI to the X-th ySegmentCoordO \ \ This works because the call to GetSectionCoord already \ stored the Y-th yTrackSectionI in the X-th \ ySegmentCoordI, and the following now copies that into \ the X-th ySegmentCoordO
Name: CopySectionData [Show more] Type: Subroutine Category: Track geometry Summary: Copy a 16-bit y-coordinate from the track section data Deep dive: Building a 3D track from sections and segments
Context: See this subroutine on its own page References: This subroutine is called as follows: * GetTrackSegment (Part 3 of 3) calls CopySectionData

Arguments: X Copy X-th (ySegmentCoordIHi ySegmentCoordILo) to (ySegmentCoordOHi ySegmentCoordOLo)
.CopySectionData LDA ySegmentCoordILo,X \ Copy the following 16-bit coordinate: STA ySegmentCoordOLo,X \ \ * The X-th ySegmentCoordI to the X-th ySegmentCoordO \ \ starting with the low byte LDA ySegmentCoordIHi,X \ And then the high byte STA ySegmentCoordOHi,X RTS \ Return from the subroutine
Name: GetPlayerIndex [Show more] Type: Subroutine Category: Track geometry Summary: Set the index for the player's segment in the track section buffer to be 32 segments behind the front segment Deep dive: Data structures for the track calculations
Context: See this subroutine on its own page References: This subroutine is called as follows: * GetTrackSegment (Part 3 of 3) calls GetPlayerIndex
.GetPlayerIndex LDA frontSegmentIndex \ Set A = the front track segment index * 3 - 96 SEC \ SBC #96 \ So this is 32 segments before the front track segment \ in the track segment buffer, as each buffer entry has \ three bytes BPL zsta1 \ If A < 0, set A = A + 120, so the index number wraps CLC \ around to the end of the stack ADC #120 .zsta1 STA playerSegmentIndex \ Set playerSegmentIndex to the updated index RTS \ Return from the subroutine
Name: GetFirstSegment [Show more] Type: Subroutine Category: Track geometry Summary: Get the track section coordinates and flags from the track data and populate the first track segment Deep dive: Data structures for the track calculations
Context: See this subroutine on its own page References: This subroutine is called as follows: * GetTrackSegment (Part 1 of 3) calls GetFirstSegment

This routine is called when we move into a new track section, and is used to populate the front track segment with the data from the start of the new track section.
Arguments: sectionBehind The number * 8 of the track section behind us, for when we are facing backwards frontSegmentIndex The index * 3 of the new front track segment in the track segment buffer
Returns: sectionListSize Bits 0-2 from trackSectionData, or 2 if we are facing backwards xSegmentCoordI Start coordinates for the inside of the track section, stored in the new front track segment xSegmentCoordO Start coordinates for the outside of the track section, stored in the new front track segment thisVectorNumber The vector number from the start of the track section thisSectionFlags The track section flags for the section containing the new front track segment segmentFlags Zeroed for the new front track segment
.GetFirstSegment LDX frontSegmentIndex \ Set X to the index * 3 of the front track segment in \ the track segment buffer LDY #6 \ Set Y = 6 STY newSectionFetched \ Set newSectionFetched to a non-zero value to indicate \ that we have just fetched a new section (as the \ GetFirstSegment routine is only called when the \ player's car enters a new section LDA resetSectionList \ If resetSectionList = 0, then we do not need to reset BEQ getf1 \ the track section list, so jump to getf1 to skip the \ following instruction \ If we get here then resetSectionList is non-zero, \ which means we need to reset the track section list STY sectionListValid \ Set sectionListValid = 6 to indicate that the track \ section list contains no valid entries, so the list \ gets regenerated over the coming iterations .getf1 LDA directionFacing \ If our car is facing backwards, jump to getf2 BMI getf2 LDY objTrackSection+23 \ Set Y to the number * 8 of the track section for the \ front segment of the track segment buffer JSR GetSectionCoords \ Copy the start coordinates for the track section \ into xSegmentCoordI and xSegmentCoordO, and set \ thisVectorNumber to trackSectionFrom LDA trackSectionData,Y \ Set A = trackSectionData for the track section, so \ bits 0-2 can be set as the value for sectionListSize \ below JMP getf3 \ Jump to getf3 to skip the following .getf2 LDY sectionBehind \ Set Y to the number * 8 of the track section in \ sectionBehind JSR GetSectionCoords \ Copy the start coordinates for the track section \ into xSegmentCoordI and xSegmentCoordO, and set \ thisVectorNumber to trackSectionFrom JSR UpdateVectorNumber \ Update thisVectorNumber to the next vector along the \ track in the direction we are facing LDA #2 \ Set A = 2, to use as the value for sectionListSize \ below .getf3 AND #%00000111 \ Set sectionListSize = bits 0-2 from A STA sectionListSize LDY objTrackSection+23 \ Set Y to the number * 8 of the track section for the \ new front segment LDA trackSectionFlag,Y \ Set thisSectionFlags to the track section flags for STA thisSectionFlags \ the section containing the new front track segment LDA #0 \ Zero the flags for the front track segment STA segmentFlags,X RTS \ Return from the subroutine
Name: ShuffleSectionList [Show more] Type: Subroutine Category: Track geometry Summary: Shuffle the track section list along by one position Deep dive: Data structures for the track calculations
Context: See this subroutine on its own page References: This subroutine is called as follows: * GetSectionAngles (Part 1 of 3) calls ShuffleSectionList

The track section list lives in the first six bytes of the following variable blocks, and contains data on a number of track sections that we use when calculating the positions of the track verges: * xVergeLeftLo * xVergeLeftHi * yVergeLeft * xVergeRightLo * xVergeRightHi * yVergeRight This routine shuffles the list along by one position, so we can insert a new track section into position 0 while pushing the data in position 5 off the end of the list. The remaining bytes in the blocks are used to store track segment data.
.ShuffleSectionList LDX #44 \ Set X = 44 so we start by shuffling the first batch: \ \ * xVergeLeftLo+0-4 to xVergeLeftLo+1-5 \ \ * xVergeLeftHi+0-4 to xVergeLeftHi+1-5 \ \ * yVergeLeft+0-4 to yVergeLeft+1-5 \ \ This works because: \ \ * xVergeRightLo + 40 = xVergeLeftLo \ \ * xVergeRightHi + 40 = xVergeLeftHi \ \ * yVergeRight + 40 = yVergeLeft .shuf1 LDA xVergeRightLo,X \ Shuffle the X-th byte of xVergeRightLo up by one STA xVergeRightLo+1,X LDA xVergeRightHi,X \ Shuffle the X-th byte of xVergeRightHi up by one STA xVergeRightHi+1,X LDA yVergeRight,X \ Shuffle the X-th byte of yVergeRight up by one STA yVergeRight+1,X CPX #40 \ If X <> 40 then we are either still shuffling the BNE shuf2 \ first batch and haven't yet done the last shuffle, or \ we are already shuffling the second batch, so in \ either case jump to shuf2 to skip the following LDX #5 \ We have just done the last shuffle in the first batch, \ so set X = 5 so we now shuffle the second batch: \ \ * xVergeRightLo+0-4 to xVergeRightLo+1-5 \ \ * xVergeRightHi+0-4 to xVergeRightHi+1-5 \ \ * yVergeRight+0-4 to yVergeRight+1-5 .shuf2 DEX \ Decrement the loop counter BPL shuf1 \ Loop back until we have shuffled all the bytes in both \ batches LDA #6 \ Set sectionListStart = 6 - sectionListSize SEC \ SBC sectionListSize \ so sectionListStart is the start index of the track STA sectionListStart \ section list, as the list ends at index 6 JSR IncSectionPointers \ Update the list pointer variables so the new entry is \ not marked as valid RTS \ Return from the subroutine
Name: IncSectionPointers [Show more] Type: Subroutine Category: Track geometry Summary: Increment the track section list pointers following a shuffle Deep dive: Data structures for the track calculations
Context: See this subroutine on its own page References: This subroutine is called as follows: * ShuffleSectionList calls IncSectionPointers

This routine increments sectionListValid and sectionListPointer so they move along with the newly shuffled track section list.
.IncSectionPointers LDX sectionListValid \ Set X = sectionListValid + 1 INX \ \ If the whole section list is valid, then this marks \ the first entry in the list as invalid, which we want \ to do as we just shuffled the list and inserted a \ dummy entry into the start of the list \ \ If the whole section is not valid, then this moves \ the valid pointer along with the shuffled entries CPX #6 \ If X < 6, then the new value of sectionListValid is BCC incp1 \ within the list, so jump to incp1 to skip the \ following LDX #6 \ Set X = 6, so the maximum value of sectionListValid \ is 6 .incp1 CPX sectionListStart \ If X >= sectionListStart, then the new value of BCS incp2 \ sectionListValid is within the list, so jump to incp2 \ to skip the following LDX sectionListStart \ Set X = sectionListStart, so the minimum value of \ sectionListValid is sectionListStart, at the start of \ the list .incp2 STX sectionListValid \ Store the updated value of X in sectionListValid, so \ we mark the new entry that we shuffled in as invalid LDX sectionListPointer \ Set X = sectionListPointer + 1 INX \ Fall through into SetSectionPointers to set the track \ section list pointer to the new value, i.e. increment \ the pointer so it moves along with the shuffled values
Name: SetSectionPointers [Show more] Type: Subroutine Category: Track geometry Summary: Set the track section list pointer to a specific value and update the validity pointer accordingly
Context: See this subroutine on its own page References: This subroutine is called as follows: * GetSectionAngles (Part 3 of 3) calls SetSectionPointers

Arguments: X The new value for sectionListPointer
.SetSectionPointers INX \ Set X = X + 1 \ \ So X points to the entry above the proposed pointer \ value in the list CPX sectionListValid \ If X >= sectionListValid, then this entry is valid, so BCS secp1 \ jump to secp1 to skip the following STX sectionListValid \ Set sectionListValid = X, so the value at the proposed \ pointer is flagged as not valid, but the value above \ it is valid .secp1 DEX \ Set X = X - 1 \ \ so X is back to its original value, the proposed value \ of sectionListPointer CPX sectionListStart \ If X >= sectionListStart, then the proposed pointer BCS secp2 \ value is not before the start of the list, so jump to \ secp2 to skip the following LDX #5 \ The proposed pointer is not within the list, so set \ X = 5, so we set sectionListPointer to 5 below .secp2 CPX #6 \ If X < 6, then the proposed pointer value is not past BCC secp3 \ the end of the list, so jump to secp3 to skip the \ following LDX #5 \ The proposed pointer is past the end of the list, so \ set X = 5, so we set sectionListPointer to 5 below .secp3 STX sectionListPointer \ Store the updated value of X in sectionListPointer RTS \ Return from the subroutine
Name: MovePlayerForward [Show more] Type: Subroutine Category: Car geometry Summary: Move the player's car forwards by one track segment and add the segment to the track segment buffer
Context: See this subroutine on its own page References: This subroutine is called as follows: * MovePlayerSegment calls MovePlayerForward
.MovePlayerForward CLC \ Clear the C flag so the call to MovePlayer moves the \ player's car in the direction it is pointing JSR MovePlayer \ Move the player's car forwards by one track segment \ Fall through into GetTrackSegment to set up the \ next track segment
Name: GetTrackSegment (Part 1 of 3) [Show more] Type: Subroutine Category: Track geometry Summary: Set up the next track segment in the track segment buffer Deep dive: Building a 3D track from sections and segments Data structures for the track calculations Corner markers
Context: See this subroutine on its own page References: This subroutine is called as follows: * PlaceCarsOnTrack calls GetTrackSegment * TurnPlayerAround calls GetTrackSegment

This routine calculates the coordinates, flags and cornering data for the next track segment. It does this in three parts: 1. Initialise up the new track segment, and fetch the segment vector for the new track segment 2. Set the flags for the new track segment 3. Add the segment vector from part 1 to the coordinates for the current track segment to get the coordinates for the next track segment This part does the following: * Move the index on to the next track segment * Initialise the new track segment if we are entering a new track section * Set pastHalfway * Set (SS T), (TT U) and (UU V) to the xTrackSegmentI, yTrackSegmentI and zTrackSegmentI vectors
.GetTrackSegment LDA frontSegmentIndex \ Set A to the index * 3 of the front track segment in \ the track segment buffer STA prevSegmentIndex \ Store A in prevSegmentIndex, as we are about to move \ on to the next track segment CLC \ Set A = frontSegmentIndex + 3 ADC #3 \ \ to move on to the next track segment ahead of the \ current front segment in the track segment buffer, \ which will become the new front segment CMP #120 \ If A < 120, then we haven't reached the end of the BCC gets1 \ track segment buffer, so jump to gets1 to store the \ updated value LDA #0 \ We just reached the end of the track segment buffer, \ so set A = 0 to wrap round to the start .gets1 STA frontSegmentIndex \ Set frontSegmentIndex to the index * 3 of the new \ front track segment LDX #23 \ Set X to 23, the object number we use to store the \ front segment of the track segment buffer LDA directionFacing \ If our car is facing backwards, jump to gets3 BMI gets3 LDA trackSectionCount \ Set A to half the total number of track sections * 4 LSR A AND #%11111000 \ Reduce A to the nearest multiple of 8 CMP objTrackSection,X \ If A <> number of the track section * 8 for the front BNE gets2 \ segment, then we are not halfway through all the track \ sections, so jump to gets2 \ If we get here then the front segment is at the \ halfway point round the track in terms of track \ section numbers LDA #1 \ Set pastHalfway = 1, to denote that the front segment STA pastHalfway \ is in the second half of the track .gets2 JSR MoveObjectForward \ Move the front segment forwards by one segment, \ setting the C flag if this moves the driver into the \ next track section BCC gets4 \ If the front segment is still in the same track \ section, jump to gets4 JSR GetFirstSegment \ Otherwise we just moved into a new track section, so \ get the track section coordinates and flags from the \ track data and populate the first track segment, \ setting thisVectorNumber to the number of the segment \ vector for the new track segment JMP gets13 \ Jump to gets13 to finish setting up the track segment, \ skipping the part that sets the coordinates and flags, \ as those have just been set when creating the entry \ for the new track section .gets3 \ If we get here then our car is facing backwards LDA objTrackSection+23 \ Set sectionBehind to the number * 8 of the track STA sectionBehind \ section for the front segment JSR MoveObjectBack \ Move the front segment backwards along the track, \ setting the C flag if this moves the driver into the \ next track section BCC gets4 \ If the front segment is still in the same track \ section, jump to gets4 JSR GetFirstSegment \ Otherwise we just moved into a new track section, so \ get the track section coordinates and flags from the \ track data and populate the first track segment, \ setting thisVectorNumber to the number of the segment \ vector for the new track segment .gets4 LDY thisVectorNumber \ Set Y to the number of the segment vector for the new \ track segment JSR GetSegmentVector \ Set (SS T), (TT U) and (UU V) to the coordinates of \ the segment vector for the new track segment, so: \ \ [ (SS T) ] [ xTrackSegmentI ] \ [ (TT U) ] = [ yTrackSegmentI ] \ [ (UU V) ] [ zTrackSegmentI ] \ \ In other words, they contain the vector that we need \ to add to the previous track segment's coordinates in \ order to get the coordinates for the new track segment \ \ Or, even simpler, they contain the vector from the \ previous track segment's coordinates to the new one
Name: GetTrackSegment (Part 2 of 3) [Show more] Type: Subroutine Category: Track geometry Summary: Set the flags for the new front segment in the track segment buffer Deep dive: Building a 3D track from sections and segments Data structures for the track calculations Corner markers
Context: See this subroutine on its own page References: No direct references to this subroutine in this source file

This part does the following: * Set the flags for the new front track segment in the track segment buffer Set the flags for the new track segment to the flags for the track section from the track data, but with these changes: Curve: * If the new front segment is not exactly halfway through the section, clear bits 3-5, to hide all corner markers (so they only get shown halfway through the section) Straight: * If the new front segment's segment number within the track section is not in the range 1 to 9, clear bits 1, 2 to show a black-and-white verge * If the distance yet to cover in this section = 7, leave bit 5 alone, so if red corner markers are configured, they are left as red * If the distance yet to cover in this section = 14 or 21, clear bit 5 to show all corner markers in white * If the distance yet to cover in this section <> 7, 14 or 21, clear bits 3-5, to hide all corner markers
LDX frontSegmentIndex \ Set X to the index * 3 of the front track segment in \ the track segment buffer LDA thisSectionFlags \ Store bit 0 of the current section's flags on the LSR A \ stack using the C flag PHP LDA thisSectionFlags \ Set A to the current section's flags BCS gets6 \ If bit 0 of the current section's flag byte is set, \ then this is a curved section, so jump to gets6 with \ A set to the current section's flags, so the new \ track segment retains the same flags \ If we get here then this is a straight track section LDY objSectionSegmt+23 \ Set Y to the new front segment's segment number within \ the track section CPY #1 \ If Y < 1, jump to gets5 BCC gets5 CPY #10 \ If Y < 10, jump to gets6 with A set to BCC gets6 \ thisSectionFlags .gets5 \ If we get here then Y < 1 or Y >= 10 AND #%11111001 \ Clear bits 1 and 2 of the current section's flags in A \ to give this segment a black-and-white verge .gets6 STA W \ Store A in W, so W contains: \ \ * The current section's flags if this is a curve, or \ if this is a straight and the new segment's number \ within the track section is in the range 1 to 9 \ \ * The current section's flags with bits 1 and 2 \ cleared otherwise (i.e. if this is a straight and \ the new segment's number within the track section \ is not in the range 1 to 9) \ \ We use W to build the flags for the new track segment LDY objTrackSection+23 \ Set Y to the number * 8 of the track section for the \ new front segment LDA trackSectionSize,Y \ Set A to the size of the track section for the new \ front segment PLP \ If bit 0 of the current section's flag byte is clear, BCC gets7 \ then this is a straight section, so jump to gets7 with \ A set to the size of the track section for the new \ front segment \ If we get here then this is a curved track section LSR A \ Set Y = A / 2 TAY \ \ So Y contains half the size of the curved section LDA W \ Set A = W, which contains the flags we are building \ for the new track segment CPY objSectionSegmt+23 \ If Y = the new front segment's number within the track BEQ gets10 \ section, then the driver is exactly halfway round the \ curve, so jump to gets10 to store A in the track \ segment's flags BNE gets8 \ Otherwise jump to gets8 to clear bits 3-5 of A before \ storing it in the track segment's flags .gets7 \ If we get here then this is a straight section and A \ is set to the size of the track section for the new \ front segment SEC \ Set Y = A - the new front segment's number within the SBC objSectionSegmt+23 \ track section TAY \ \ So Y contains the length of the track section between \ the new front segment and the end of the section LDA W \ Set A = W, which contains the flags we are building \ for the new track segment CPY #7 \ If Y = 7, jump to gets10 to store A in the track BEQ gets10 \ segment's flags CPY #14 \ If Y = 14 or 21, jump to gets9 to clear bit 5 of A BEQ gets9 \ and store it in the track segment's flags CPY #21 BEQ gets9 \ Otherwise we clear bits 3-5 of A and store it in the \ track segment's flags .gets8 AND #%11100111 \ Clear bits 3 and 4 of A, so we don't show any corner \ markers for this segment .gets9 AND #%11011111 \ Clear bit 5 of A, so any markers for this segment are \ shown in white .gets10 AND thisSectionFlags \ Clear any bits in A that are already clear in the \ current section's flags, to ensure that the track \ segment's flags only have flags set if they are set in \ the track section's flags STA segmentFlags,X \ Set the flags for the new track segment to A
Name: GetTrackSegment (Part 3 of 3) [Show more] Type: Subroutine Category: Track geometry Summary: Set the inner and outer track coordinates for the new track segment Deep dive: Building a 3D track from sections and segments Data structures for the track calculations Corner markers
Context: See this subroutine on its own page References: No direct references to this subroutine in this source file

This part calculates the inner track section coordinates for the new track segment, as follows: [ xSegmentCoordI ] [ previous xSegmentCoordI ] [ xTrackSegmentI ] [ ySegmentCoordI ] = [ previous ySegmentCoordI ] + [ yTrackSegmentI ] [ zSegmentCoordI ] [ previous zSegmentCoordI ] [ zTrackSegmentI ] It then calculates the outer track section coordinates for the new track segment, as follows: [ xSegmentCoordO ] [ xSegmentCoordI ] [ xTrackSegmentO ] [ ySegmentCoordO ] = [ ySegmentCoordI ] + [ 0 ] * 4 [ zSegmentCoordO ] [ zSegmentCoordI ] [ zTrackSegmentO ]
LDY prevSegmentIndex \ Set Y to the index * 3 of the previous track segment JSR AddVectors \ Add the (SS T), (TT U) and (UU V) vectors to the \ inner track section coordinates for the previous \ track segment and store the result in the current \ track segment: \ \ [ (SS T) ] \ this entry = previous entry + [ (TT U) ] \ [ (UU V) ] \ \ This correctly sets the inner track coordinates for \ the new track segment, as we set the (SS T), (TT U) \ and (UU V) vectors at the end of part 1 to the vector \ from the previous track segment's coordinates to the \ new one \ \ In other words, this does the following: \ \ [ xSegmentCoordI ] [ previous xSegmentCoordI ] \ [ ySegmentCoordI ] = [ previous ySegmentCoordI ] \ [ zSegmentCoordI ] [ previous zSegmentCoordI ] \ \ [ xTrackSegmentI ] \ + [ yTrackSegmentI ] \ [ zTrackSegmentI ] JSR CopySectionData \ Copy the X-th ySegmentCoordI to ySegmentCoordO \ We now set the outer track coordinates for the new \ track segment, as follows: \ \ [ xSegmentCoordO ] [ xSegmentCoordI ] \ [ ySegmentCoordO ] = [ ySegmentCoordI ] \ [ zSegmentCoordO ] [ zSegmentCoordI ] \ \ [ xTrackSegmentO ] \ + [ 0 ] * 4 \ [ zTrackSegmentO ] \ \ Note that we don't have to do the y-coordinate \ addition, as ySegmentCoordO was already set to \ ySegmentCoordI when the track segment was set up LDY thisVectorNumber \ Set Y to the number of the segment vector for the new \ track segment LDA #0 \ Set SS = 0, to use as the high byte in (SS A) below STA SS STA UU \ Set UU = 0, to use as the high byte in (UU A) below LDA xTrackSegmentO,Y \ Set A = the 3D x-coordinate of the outer segment \ vector for the new track segment BPL gets11 \ If A is negative, decrement SS to &FF so (SS A) has DEC SS \ the correct sign .gets11 ASL A \ Set (SS A) = (SS A) * 4 ROL SS \ = xTrackSegmentO * 4 ASL A ROL SS CLC \ Add the inner track section x-coordinate and the outer ADC xSegmentCoordILo,X \ x-coordinate * 4 to get the outer x-coordinate for the STA xSegmentCoordOLo,X \ new track segment, starting with the low bytes LDA SS \ And then the high bytes ADC xSegmentCoordIHi,X STA xSegmentCoordOHi,X LDA zTrackSegmentO,Y \ Set A = the z-coordinate of the outer segment vector \ for the new track segment BPL gets12 \ If A is negative, decrement UU to &FF so (UU A) has DEC UU \ the correct sign .gets12 ASL A \ Set (UU A) = (UU A) * 4 ROL UU \ = zTrackSegmentO * 4 ASL A ROL UU CLC \ Add the inner track section z-coordinate and the outer ADC zSegmentCoordILo,X \ z-coordinate * 4 to get the outer z-coordinate for the STA zSegmentCoordOLo,X \ new track segment, starting with the low bytes LDA UU \ And then the high bytes ADC zSegmentCoordIHi,X STA zSegmentCoordOHi,X JSR UpdateCurveVector \ If this is a curved track section, update the value of \ thisVectorNumber to the next segment vector along the \ track in the direction we are facing .gets13 LDX frontSegmentIndex \ Set X to the index * 3 of the front track segment in \ the track segment buffer LDA thisVectorNumber \ Set segmentVector for the new track segment to the STA segmentVector,X \ number of the segment vector for the track section \ (which will be unchanged from the previous section if \ this is a straight, or the next segment vector if this \ is a curve) JSR GetPlayerIndex \ Update the index for the segment containing the player \ in the track segment buffer JSR GetSegmentSteering \ Set segmentSteering for the new track segment to the \ optimum steering to apply at this point in the section RTS \ Return from the subroutine
Name: UpdateCurveVector [Show more] Type: Subroutine Category: Track geometry Summary: Move to the next segment vector along in the direction we are facing, but only for curved track sections Deep dive: Building a 3D track from sections and segments
Context: See this subroutine on its own page References: This subroutine is called as follows: * GetTrackSegment (Part 3 of 3) calls UpdateCurveVector * TurnPlayerAround calls UpdateCurveVector
.UpdateCurveVector LDA thisSectionFlags \ If bit 0 of the track section's flag byte is clear, AND #1 \ then this is a straight track section, so return from BEQ ChangeDirection-1 \ the subroutine (as ChangeDirection-1 contains an RTS) \ Otherwise this is a curved track section, so fall \ through into UpdateVectorNumber to update the value \ of thisVectorNumber to the number of the next vector \ along the track
Name: UpdateVectorNumber [Show more] Type: Subroutine Category: Track geometry Summary: Move to the next segment vector along the track in the direction we are facing Deep dive: Building a 3D track from sections and segments
Context: See this subroutine on its own page References: This subroutine is called as follows: * GetFirstSegment calls UpdateVectorNumber

Returns: thisVectorNumber Set to the number of the next segment vector along the track in the direction in which we are facing
.UpdateVectorNumber LDA directionFacing \ If our car is facing backwards, jump to uvec1 BMI uvec1 LDY thisVectorNumber \ Set Y to the current segment vector number INY \ Increment Y to point to the next segment vector number \ along the track CPY trackVectorCount \ If Y <> trackVectorCount, then we have not reached the BNE uvec3 \ last vector, so jump to uvec3 to store the new value \ of thisVectorNumber LDY #0 \ If we get here then we have reached the last vector, \ so set Y = 0 to wrap around to the first vector BEQ uvec3 \ Jump to uvec3 to set thisVectorNumber = 0 (this BEQ is \ effectively a JMP as Y is always zero) .uvec1 \ If we get here then our car is facing backwards LDY thisVectorNumber \ Set Y to the current segment vector number BNE uvec2 \ If Y <> 0, then we are not on the first segment \ vector, so jump to uvec2 to decrement to the previous \ vector LDY trackVectorCount \ Set Y = trackVectorCount, so we wrap around to the \ last vector .uvec2 DEY \ Decrement Y to point to the previous segment vector \ number, i.e. backwards along the track .uvec3 STY thisVectorNumber \ Update thisVectorNumber with the new value that we set \ above .uvec4 RTS \ Return from the subroutine
Name: ChangeDirection [Show more] Type: Subroutine Category: Car geometry Summary: Update the track segment buffer when the player's car spins so it changes the direction in which it is facing along the track
Context: See this subroutine on its own page References: This subroutine is called as follows: * MovePlayerSegment calls ChangeDirection * UpdateCurveVector calls via ChangeDirection-1

Other entry points: ChangeDirection-1 Contains an RTS
.ChangeDirection LDA #6 \ Set sectionListValid = 6, to invalidate the contents STA sectionListValid \ of the track section list (so it gets repopulated \ by the GetTrackAndMarkers routine) LDX #64 \ Set X = 64, so the call to TurnPlayerAround \ initialises 64 track segments in the new direction STX debugSpinning \ Set debugSpinning = 64 (this value is never read and \ is not used anywhere, so perhaps it was used for \ debugging purposes, or was left over from code that \ was subsequently removed?) JSR TurnPlayerAround \ Turn the player around and initialise 64 track \ segments in the new direction LDA #0 \ Set debugSpinning = 0 (which has no effect) STA debugSpinning RTS \ Return from the subroutine
Name: MovePlayerBack [Show more] Type: Subroutine Category: Car geometry Summary: Move the player's car backwards by one track segment and update the track segment buffer
Context: See this subroutine on its own page References: This subroutine is called as follows: * MovePlayerSegment calls MovePlayerBack

This routine reverses the player, updating the track segment buffer in both directions.
.MovePlayerBack SEC \ Set the C flag so the call to MovePlayer moves the \ player's car in the opposite direction to which it is \ pointing JSR MovePlayer \ Drive the player's car backwards by one track segment LDX #40 \ Set X = 40, so the call to TurnPlayerAround \ initialises 64 track segments in the new direction STX resetSectionList \ Set resetSectionList to a non-zero value, so if the \ calls to TurnPlayerAround need to fetch load a new \ track section, then we reset the track section list JSR TurnPlayerAround \ Turn the player around and initialise 40 track \ segments in the new direction LDX #39 \ Set X = 39, so the call to TurnPlayerAround \ initialises 64 track segments in the new direction JSR TurnPlayerAround \ Turn the player back around and initialise 39 track \ segments in the new direction LDA #0 \ Set resetSectionList = 0, so future calls to the STA resetSectionList \ TurnPlayerAround routine will update the track section \ list as normal RTS \ Return from the subroutine
Name: TurnPlayerAround [Show more] Type: Subroutine Category: Car geometry Summary: Turn the player around and initialise the specified number of track segments in the new direction
Context: See this subroutine on its own page References: This subroutine is called as follows: * ChangeDirection calls TurnPlayerAround * MovePlayerBack calls TurnPlayerAround

Arguments: X The number of track segments to initialise in the new direction
Returns: directionFacing Bit 7 is flipped to point us in the opposite direction
.TurnPlayerAround LDA directionFacing \ Flip bit 7 of directionFacing to denote that our car EOR #%10000000 \ is facing in the other direction STA directionFacing JSR UpdateCurveVector \ If this is a curved track section, update the value of \ thisVectorNumber to the next segment vector along the \ track in the new direction we are facing STX segmentCounter \ We now want to initialise X track segments in the new \ direction, so set a loop counter in segmentCounter \ that starts from X and counts down .turn1 JSR GetTrackSegment \ Initialise the next track segment in the track segment \ buffer DEC segmentCounter \ Decrement the loop counter BNE turn1 \ Loop back until we have set all X track segments RTS \ Return from the subroutine
Name: MovePlayer [Show more] Type: Subroutine Category: Car geometry Summary: Move the player's car forwards or backwards by one segment
Context: See this subroutine on its own page References: This subroutine is called as follows: * MovePlayerBack calls MovePlayer * MovePlayerForward calls MovePlayer

Arguments: C flag Controls the direction of movement: * Clear = move the car in the direction it is pointing (i.e. drive forwards) * Set = reverse the car
.MovePlayer LDX currentPlayer \ Set X to the driver number of the current player ROR A \ If just one of the C flag and bit 7 of directionFacing EOR directionFacing \ is set (but not both), jump to play1 to move the car BMI play1 \ backwards along the track \ \ In other words, we move backwards if: \ \ * C is clear and the car is facing backwards along \ the track, in which case it is driving forwards \ but going backwards around the track \ \ * C is set and the car is facing forwards along the \ track, in which case it is reversing along the \ track while facing forwards JSR MoveObjectForward \ Move the current player forwards by one segment RTS \ Return from the subroutine .play1 JSR MoveObjectBack \ Move current player backwards by one segment RTS \ Return from the subroutine
Name: GetSegmentVector [Show more] Type: Subroutine Category: Track geometry Summary: Fetch a segment vector from the track data file Deep dive: Building a 3D track from sections and segments
Context: See this subroutine on its own page References: This subroutine is called as follows: * BuildCarObjects (Part 3 of 3) calls GetSegmentVector * GetTrackSegment (Part 1 of 3) calls GetSegmentVector

Arguments: Y The index of the coordinate in the track data blocks
Returns: (SS T) The Y-th entry from xTrackSegmentI as a 16-bit signed integer (TT U) The Y-th entry from yTrackSegmentI as a 16-bit signed integer (UU V) The Y-th entry from zTrackSegmentI as a 16-bit signed integer
.GetSegmentVector LDA #0 \ Zero the high bytes of (SS T), (TT U) and (UU V) STA SS STA TT STA UU LDA xTrackSegmentI,Y \ Set T = the Y-th entry from xTrackSegmentI STA T BPL coor1 \ If the byte we just fetched is negative, decrement DEC SS \ the high byte in SS to &FF, so (SS T) has the correct \ sign .coor1 LDA yTrackSegmentI,Y \ Set U = the Y-th entry from yTrackSegmentI STA U BPL coor2 \ If the byte we just fetched is negative, decrement DEC TT \ the high byte in TT to &FF, so (TT U) has the correct \ sign .coor2 LDA zTrackSegmentI,Y \ Set V = the Y-th entry from zTrackSegmentI STA V BPL coor3 \ If the byte we just fetched is negative, decrement DEC UU \ the high byte in UU to &FF, so (UU V) has the correct \ sign .coor3 LDA directionFacing \ If our car is facing forwards, jump to coor5 to return BEQ coor5 \ from the subroutine \ We are facing backwards, so we need to negate the \ coordinate we just fetched LDX #2 \ We are about to negate the following 16-bit variables: \ \ (SS T) \ (TT U) \ (UU V) \ \ so set a counter in X to use as an index that loops \ through 2, 1 and 0, as: \ \ (TT U) = (SS+1 U+1) \ (UU V) = (SS+2 U+2) \ \ The following comments are for (SS T), but the same \ process applies for (TT U) and (UU V) .coor4 LDA #0 \ Set (SS T) = 0 - (SS T) SEC \ SBC T,X \ starting with the low bytes STA T,X LDA #0 \ And then the high bytes SBC SS,X STA SS,X DEX \ Decrement the loop counter to move on to the next \ variable BPL coor4 \ Loop back until we have negated all three 16-bit \ variables .coor5 RTS \ Return from the subroutine
Name: MoveObjectForward [Show more] Type: Subroutine Category: 3D objects Summary: Move a specified object forwards along the track by one segment
Context: See this subroutine on its own page References: This subroutine is called as follows: * GetTrackSegment (Part 1 of 3) calls MoveObjectForward * MoveCars (Part 1 of 2) calls MoveObjectForward * MovePlayer calls MoveObjectForward * PlaceCarsOnTrack calls MoveObjectForward

Moves object X forwards by one segment, updating the section number if we cross into a new section. Also updates the lap number and lap time, but only if this is a driver and bit 7 of updateLapTimes is clear.
Arguments: X Driver number (0-23) updateLapTimes If bit 7 is set, the call to UpdateLaps has no effect, so we do not update the lap number or lap time
Returns: C flag Whether the driver has moved into a new section: * Clear if the driver is still within the same track section as before * Set if the driver has now moved into the next track section
.MoveObjectForward LDY objTrackSection,X \ Set Y to the track section number * 8 for object X LDA objSectionSegmt,X \ Set A = objSectionSegmt + 1 CLC \ ADC #1 \ This increments the section segment counter, which \ keeps track of the object's segment number in the \ current track section, so it moves forward one segment CMP trackSectionSize,Y \ If A < Y-th trackSectionSize, then the object is still PHP \ within the current track section, so clear the C flag \ and store it on the stack, otherwise the object has \ now reached the end of the current section, so set the \ C flag and store it on the stack BCC fore2 \ If A < Y-th trackSectionSize, then the object is still \ within the current track section, so jump to fore2 to \ update the track section counter with the new value TYA \ Set A = Y + 8 CLC \ ADC #8 \ So A contains the track section number * 8 of the next \ track section CMP trackSectionCount \ If A < trackSectionCount then this isn't the last BCC fore1 \ section before we wrap round to zero again, so jump to \ fore1 to skip the following instruction LDA #0 \ Set A = 0, so the track section number wraps round to \ zero when we reach the last section .fore1 STA objTrackSection,X \ Update the object's track section number to the new \ one that they just moved into LDA #0 \ The object just entered a new track section, so we \ need to zero the section counter, which keeps track of \ how far through the current section the object is, so \ set A to 0 to set as its new value below .fore2 STA objSectionSegmt,X \ Set the track section counter to the new value \ We now need to increment the object's segment number \ in (objectSegmentHi objectSegmentLo) INC objectSegmentLo,X \ Increment (objectSegmentHi objectSegmentLo) for object \ X, starting with the low byte BNE fore3 \ And then the high byte, if the low byte overflows INC objectSegmentHi,X .fore3 LDA objectSegmentLo,X \ If objectSegment <> trackLength, then the object has CMP trackLength \ not yet reached the end of the track, so jump to fore4 BNE fore4 \ to return from the subroutine as we are done LDA objectSegmentHi,X CMP trackLength+1 BNE fore4 LDA #0 \ The object has just reached the end of the track, so STA objectSegmentLo,X \ set (objectSegmentHi objectSegmentLo) = 0 to wrap STA objectSegmentHi,X \ round to the start again JSR UpdateLaps \ Increment the lap number and lap times for object X, \ for when this object is a car .fore4 PLP \ Retrieve the C flag from the stack so we can return it \ from the subroutine, so it is clear if the driver is \ still within the same track section, set otherwise RTS \ Return from the subroutine
Name: MoveObjectBack [Show more] Type: Subroutine Category: 3D objects Summary: Move a specified object backwards along the track by one segment
Context: See this subroutine on its own page References: This subroutine is called as follows: * GetTrackSegment (Part 1 of 3) calls MoveObjectBack * MovePlayer calls MoveObjectBack * PlaceCarsOnTrack calls MoveObjectBack

Moves object X backwards by one segment, updating the section number if we cross into a new section. Also updates the lap number if this is the current player.
Arguments: X Object number (0-23)
Returns: C flag Whether the object has moved into a new section: * Clear if the object is still within the same track section as before * Set if the object has now moved back into the previous track section
.MoveObjectBack LDY objTrackSection,X \ Set Y to the track section number * 8 for object X LDA objSectionSegmt,X \ Set A = objSectionSegmt, which keeps track of the \ object's segment number in the current track section CLC \ If A is non-zero then the object can move backwards by BNE back2 \ one step while staying in the same track section, so \ clear the C flag and jump to back2 to store this \ result on the stack \ Otherwise moving backwards will move the object into \ the previous track section TYA \ Store the current track section number * 8 in A BNE back1 \ If the track section number is non-zero, then skip the \ following instruction \ If we get here then the object is in the first track \ section and is moving backwards into the previous \ track section, so we have to wrap around to the end \ of the track to move into the very last track section LDA trackSectionCount \ Set A = trackSectionCount \ \ So A contains the number * 8 of the last track section \ plus 1 (as the track sections count from zero) .back1 \ By this point, A contains the number * 8 of the \ current track section, updated to cater for wrapping \ round at the end SEC \ Set A = A - 8 SBC #8 \ \ So A is now the number * 8 of the previous track \ section, which is where the driver is moving to STA objTrackSection,X \ Update the object's track section number to the new \ one that they just moved into TAY \ Set A to the size of the new track section, so A LDA trackSectionSize,Y \ contains the track section counter for the end of the \ new track section (so the object moves to the end of \ the new track section) SEC \ Set the C flag to indicate that the object has moved \ into a different track section .back2 PHP \ Store the C flag on the stack, which will be clear if \ the object is still within the same track section, set \ otherwise SEC \ A contains the object's segment number in the current SBC #1 \ track section, so subtract 1 to move the object \ backwards STA objSectionSegmt,X \ Set the track section counter to the new value .back3 \ We now need to decrement the object's segment number \ in (objectSegmentHi objectSegmentLo) LDA objectSegmentLo,X \ If the low byte of objectSegment for object X is BNE back4 \ non-zero, then jump to back4 to decrement the low \ byte, and we are done DEC objectSegmentHi,X \ Otherwise decrement the high byte BPL back4 \ If the high byte is positive, jump to back4 to \ decrement the low byte to &FF, and we are done \ If we get here then we just decremented the 16-bit \ value in (objectSegmentHi objectSegmentLo) into \ negative territory, so the object just moved \ backwards across the starting line, into the previous \ lap LDA trackLength \ Set objectSegment for driver X = trackLength STA objectSegmentLo,X \ LDA trackLength+1 \ So objectSegment wraps around to the segment number STA objectSegmentHi,X \ for the end of the track, as trackLength contains the \ length of the full track (in terms of segments) CPX currentPlayer \ If object X is not the current player, jump to back3 BNE back3 \ to decrement the newly wrapped segment number LDA driverLapNumber,X \ If the current lap number for the current player is BEQ back3 \ zero (i.e. this is the first lap) then jump to back3 \ to decrement the newly wrapped segment number DEC driverLapNumber,X \ Otherwise this is not the current player's first lap, \ and they just moved backwards, across the starting \ line and into the previous lap, so decrement the \ current lap number for the current player JMP back3 \ Jump to back3 to decrement the newly wrapped segment \ number .back4 DEC objectSegmentLo,X \ Decrement the low byte of the object's segment number \ to move it backwards PLP \ Retrieve the C flag from the stack so we can return it \ from the subroutine, so it is clear if the object is \ still within the same track section, set otherwise RTS \ Return from the subroutine
Name: GetSegmentSteering [Show more] Type: Subroutine Category: Tactics Summary: Calculate the optimum steering to take for the current track segment Deep dive: Tactics of the non-player drivers
Context: See this subroutine on its own page References: This subroutine is called as follows: * GetTrackSegment (Part 3 of 3) calls GetSegmentSteering

This routine populates segmentSteering for a track segment, depending on a large number of factors.
.GetSegmentSteering LDY objTrackSection+23 \ Set Y to the number * 8 of the track section for the \ front segment of the track segment buffer TYA \ Set X = Y / 8 LSR A \ LSR A \ So X contains the number of the track section for the LSR A \ front segment TAX LDA turnCounter \ If turnCounter is non-zero, then a turn is already in BNE rlin5 \ progress, so jump to rlin5 \ We get here if turnCounter is zero LDA thisSectionFlags \ If bit 0 of thisSectionFlags is set then this is a LSR A \ curved track section, so jump to rlin2 BCS rlin2 LDA objSectionSegmt+23 \ If the front segment's number within the track section CMP trackSectionTurn,Y \ is >= the trackSectionTurn for the section, jump to BCS rlin3 \ rlin3 to move on to the next track section \ If we get here, then: \ \ * turnCounter is zero \ \ * Bit 0 of thisSectionFlags is clear, so this is a \ straight track section \ \ * The front segment's number within the track \ section is less than the trackSectionTurn value \ for the section \ \ So we are not already turning from a previous call, we \ are on a straight, and we haven't yet reached the \ point at the end of the straight where we should be \ turning \ \ So now we keep on driving straight .rlin1 LDA sectionSteering,X \ Set A to the optimum steering for section X, with ORA #%01000000 \ bit 6 set, so we do not apply any steering in MoveCars \ but instead just apply acceleration or braking BNE rlin7 \ Jump to rlin7 to store A in segmentSteering for this \ section and return from the subroutine (this BNE is \ effectively a JMP as A is never zero) .rlin2 \ If we get here then this is a curved track section and \ turnCounter is zero LDA prevDriverSpeed7 \ If bit 7 of prevDriverSpeed7 is set, jump to rlin4 BMI rlin4 \ If we get here then this is a curved track section and \ turnCounter is zero, and bit 7 of prevDriverSpeed7 is \ clear .rlin3 \ If we jump here, then the front segment is past the \ trackSectionTurn point in this section, so we start \ looking at the next section for the optimum steering TYA \ Set Y = Y + 8 CLC \ ADC #8 \ So Y contains the number * 8 of the next track section TAY INX \ Increment X, so X contains the number of the next \ track section .rlin4 LDA trackSectionFlag,Y \ If bit 0 of the track section's flag byte is clear, AND #1 \ then this is a straight section, so jump to rlin1 to BEQ rlin1 \ disable steering in MoveCars and keep on driving \ straight LDA trackSectionTurn,Y \ Set turnCounter = track section's trackSectionTurn STA turnCounter BEQ rlin1 \ If the track section's trackSectionTurn is zero, jump \ to rlin1 to disable steering in MoveCars and keep on \ driving straight LDA trackDriverSpeed,Y \ Set prevDriverSpeed7 = track section's driver speed, STA prevDriverSpeed7 \ which we only use to check bit 7 AND #%01111111 \ Set prevDriverSpeed06 = bits 0-6 of the track STA prevDriverSpeed06 \ section's driver speed LDA sectionSteering,X \ Set A to the optimum steering for the track section STA previousSteering \ Set previousSteering to the optimum steering for the \ track section JMP rlin7 \ Jump to rlin7 to store A in segmentSteering for this \ section and return from the subroutine (this BNE is \ effectively a JMP as A is never zero) .rlin5 \ If we get here then turnCounter is non-zero, so a \ turn is already in progress DEC turnCounter \ Decrement the turn counter in turnCounter LDA turnCounter \ Set T = turnCounter / 8 LSR A LSR A LSR A STA T LDA turnCounter \ Set A = turnCounter - prevDriverSpeed06 SEC SBC prevDriverSpeed06 BCS rlin6 \ If the subtraction didn't underflow, i.e. \ \ turnCounter >= prevDriverSpeed06 \ \ then jump to rlin6 to set segmentSteering to \ previousSteering and return from the subroutine \ If we get here then turnCounter < prevDriverSpeed06 \ and A is negative ADC T \ Set A = A + T \ = turnCounter - prevDriverSpeed06 \ + turnCounter / 8 \ = (turnCounter * 1.125) - prevDriverSpeed06 LDA #0 \ Set A = 0 BCS rlin7 \ If the addition overflowed, then because A was \ negative, we must have the following: \ \ (turnCounter * 1.125) - prevDriverSpeed06 >= 0 \ \ So jump to rlin7 to store 0 in segmentSteering for \ this section and return from the subroutine \ \ Otherwise store previousSteering in segmentSteering \ for this section with bit 7 flipped, and return from \ the subroutine .rlin6 LDA previousSteering \ Set A = previousSteering BCS rlin7 \ If the C flag is clear, flip bit 7 of A EOR #%10000000 .rlin7 LDY frontSegmentIndex \ Set segmentSteering for the front segment in the track STA segmentSteering,Y \ segment buffer to A RTS \ Return from the subroutine
Name: ProcessDrivingKeys (Part 1 of 6) [Show more] Type: Subroutine Category: Keyboard Summary: Process joystick steering Deep dive: Computer assisted steering (CAS)
Context: See this subroutine on its own page References: This subroutine is called as follows: * MainDrivingLoop (Part 2 of 5) calls ProcessDrivingKeys

This routine scans for key presses, or joystick (when configured), and updates the following variables accordingly: * Steering: (steeringHi steeringLo) * Brake/throttle: throttleBrakeState, throttleBrake * Gear changes: gearChangeKey, gearChange, gearNumber
.ProcessDrivingKeys LDA #0 \ Set V = 0, which we will use to indicate whether any STA V \ steering is being applied (0 indicates that none is \ being applied, which we may change later) STA T \ Set T = 0, which we will use to record whether SPACE \ is being pressed, which makes the steering wheel turn \ more quickly STA gearChangeKey \ Set gearChangeKey = 0, which we will use to indicate \ whether any gear change are being applied (0 indicates \ that no gear changes are being applied, which we may \ change later) IF _ACORNSOFT OR _4TRACKS BIT configJoystick \ If bit 7 of configJoystick is clear then the joystick BPL keys2 \ is not configured, so jump to keys2 to check for key \ presses for the steering LDX #&9D \ Scan the keyboard to see if SPACE is being pressed, as JSR ScanKeyboard \ this will affect the speed of any steering changes PHP \ Store the result of the scan on the stack ELIF _SUPERIOR OR _REVSPLUS LDX #&9D \ Scan the keyboard to see if SPACE is being pressed, as JSR ScanKeyboard \ this will affect the speed of any steering changes PHP \ Store the result of the scan on the stack BIT configJoystick \ If bit 7 of configJoystick is clear then the joystick BPL keys2 \ is not configured, so jump to keys2 to check for key \ presses for the steering ENDIF LDX #1 \ Read the joystick x-axis into A and X (A is set to the JSR GetADCChannel \ high byte of the channel, X is set to the sign of A \ where 1 = negative/left, 0 = positive/right) STA U \ Store the x-axis high byte in U JSR Multiply8x8 \ Set (A T) = A * U \ = A * A \ = A^2 \ = x-axis^2 PLP \ Retrieve the result of the keyboard scan above, when \ we scanned for SPACE BEQ keys1 \ If SPACE is being pressed, jump to keys1 so the value \ of (A T) will be four times higher LSR A \ Set (A T) = (A T) / 4 ROR T \ = x-axis^2 / 4 LSR A ROR T .keys1 IF _ACORNSOFT OR _4TRACKS PHA \ Store A on the stack so we can retrieve it later ELIF _SUPERIOR OR _REVSPLUS STA U \ Set (U T) = (A T) ENDIF LDA T \ Clear bit 0 of T AND #%11111110 STA T TXA \ Set bit 0 of T to the sign bit in X (1 = left, ORA T \ 0 = right), so this sets (A T) to the correct sign STA T \ for a steering measurement IF _ACORNSOFT OR _4TRACKS PLA \ Retrieve the value of A that we stored on the stack JMP keys11 \ Jump to the end of part 2 to update the steering value \ in (steeringHi steeringLo) to (A T) ELIF _SUPERIOR OR _REVSPLUS LDA U \ Set (A T) = (U T) \ \ so (A T) contains the joystick x-axis high byte, \ squared, divided by 4 if SPACE is not being pressed, \ and converted into a sign-magnitude number with the \ sign in bit 0 (1 = left, 0 = right) JMP AssistSteering \ Jump to AssistSteering to apply computer assisted \ steering (CAS), which in turn jumps back to keys7 or \ keys11 in part 2 NOP \ These instructions are unused, and are included to NOP \ pad out the code from when the CAS code was inserted \ into the original version ENDIF
Name: ProcessDrivingKeys (Part 2 of 6) [Show more] Type: Subroutine Category: Keyboard Summary: Process keyboard steering Deep dive: Computer assisted steering (CAS)
Context: See this subroutine on its own page References: This subroutine is called as follows: * AssistSteering calls via keys7 * AssistSteering calls via keys10 * AssistSteering calls via keys11 * ProcessDrivingKeys (Part 1 of 6) calls via keys11

Other entry points: keys7 Re-entry point for the AssistSteering routine when no joystick steering is being applied keys10 Re-entry point for the AssistSteering routine when CAS is being applied to the steering keys11 Re-entry point for the AssistSteering routine if CAS is not enabled or the car is facing backwards
.keys2 LDX #&A9 \ Scan the keyboard to see if "L" is being pressed JSR ScanKeyboard BNE keys3 \ If "L" is not being pressed, jump to keys3 LDA #2 \ Set V = 2 STA V .keys3 LDX #&A8 \ Scan the keyboard to see if ";" is being pressed JSR ScanKeyboard BNE keys4 \ If ";" is not being pressed, jump to keys4 INC V \ Set V = 1 .keys4 \ By this point, we have: \ \ * V = 1 if ";" is being pressed (steer right) \ \ * V = 2 if "L" is being pressed (steer left) \ \ * V = 0 if neither is being pressed \ \ We now calculate the amount of steering change into \ (A T), so we can apply it to (steeringHi steeringLo), \ which is a sign-magnitude number with the sign bit in \ bit 0 \ \ In the following, we swap the steering change between \ (A T) and (U T) quite a bit, and in the Superior \ Software variant of the game, we also apply computer \ assisted steering (CAS) LDA #3 \ Set U = 3 STA U \ \ So (U T) = (3 0) = 768, which is the value we use for \ steering when the SPACE key is held down IF _ACORNSOFT OR _4TRACKS LDX #&9D \ Scan the keyboard to see if SPACE is being pressed JSR ScanKeyboard ELIF _SUPERIOR OR _REVSPLUS PLP \ Retrieve the result of the keyboard scan above, when \ we scanned for SPACE ENDIF BEQ keys6 \ If SPACE is being pressed, jump to keys6 with \ (U T) = 768 \ SPACE is not being pressed, so we need to calculate \ the correct value of (U T) depending on the steering \ wheel position LDA #0 \ Set A = 0 LDX #2 \ If steeringHi > 2, jump to keys5 to skip the following CPX steeringHi \ instruction BCC keys5 LDA #1 \ Set A = 1 .keys5 STA U \ Set U = A, which will be 1 if steeringHi > 2, or \ 0 otherwise LDA #128 \ Set T = 128 STA T .keys6 \ By this point, (U T) is: \ \ * (3 0) = 768 if the SPACE key is being held down \ \ * (1 128) = 384 if steeringHi > 2 \ \ * (0 128) = 128 if steeringHi <= 2 LDA V \ If V = 0 then no steering is being applied, so jump to BEQ keys7 \ keys7 CMP #3 \ If V = 3, jump to keys13 to move on to the throttle BEQ keys13 \ and brake keys without applying any steering \ If we get here then V = 1 or 2, so steering is being \ applied, so we set the sign of (U T) to match the \ position of the wheel, before jumping down to keys8 \ or keys9 EOR steeringLo \ If bit 0 of steeringLo is clear, jump to keys9 AND #1 BEQ keys9 JSR Negate16Bit+2 \ Otherwise (steeringHi steeringLo) is negative, so \ set (A T) = -(U T) JMP keys8 \ Jump to keys8 to store the negated value in (U T) .keys7 \ If we get here then no steering is being applied LDA xTyreForceNoseLo \ Set T = xTyreForceNoseLo AND %11110000 AND #%11110000 STA T LDA xTyreForceNoseHi \ Set (A T) = (xTyreForceNoseHi xTyreForceNoseLo) \ AND %11110000 \ = xTyreForceNose AND %11110000 JSR Absolute16Bit \ Set (A T) = |A T| LSR A \ Set (A T) = (A T) >> 2 ROR T \ = |A T| / 4 LSR A \ = (|xTyreForceNose| AND %11110000) / 4 ROR T IF _ACORNSOFT OR _4TRACKS CMP #12 \ If A < 12, skip the following instruction BCC keys8 LDA #12 \ Set A = 12, so A has a maximum value of 12, and |A T| \ is set to a maximum value of 12 * 256 ELIF _SUPERIOR OR _REVSPLUS CMP steeringHi \ If A < steeringHi, clear the C flag, so the following \ call to SetSteeringLimit does nothing JSR SetSteeringLimit \ If A >= steeringHi, set: \ \ (A T) = |steeringHi steeringLo| \ \ so this is the maximum value of |A T| ENDIF .keys8 STA U \ Set (U T) = (A T) .keys9 IF _ACORNSOFT OR _4TRACKS LDA steeringLo \ Set A = steeringLo ELIF _SUPERIOR OR _REVSPLUS JMP AssistSteeringKeys \ Jump to AssistSteeringKeys, which in turn jumps back \ to keys10, so this is effectively a JSR call \ \ The routine returns with A = steeringLo ENDIF .keys10 SEC \ Set (A T) = (steeringHi steeringLo) - (U T) SBC T \ STA T \ starting with the low bytes LDA steeringHi \ And then the high bytes SBC U CMP #200 \ If the high byte in A < 200, skip the following BCC keys11 \ instructions \ Otherwise the high byte in A >= 200, so we negate \ (A T) JSR Negate16Bit \ Set (A T) = -(A T) STA U \ Set (U T) = (A T) LDA T \ Flip the sign bit in bit 0 of T EOR #1 STA T LDA U \ Set (A T) = (U T) .keys11 CMP #145 \ If the high byte in A < 145, skip the following BCC keys12 \ instruction LDA #145 \ Set A = 145, so A has a maximum value of 145 .keys12 STA steeringHi \ Set (steeringHi steeringLo) = (A T) LDA T STA steeringLo
Name: ProcessDrivingKeys (Part 3 of 6) [Show more] Type: Subroutine Category: Keyboard Summary: Process joystick brake and throttle
Context: See this subroutine on its own page References: No direct references to this subroutine in this source file
.keys13 LDA leaveTrackTimer \ If leaveTrackTimer is non-zero then the leave track BNE keys19 \ timer is counting down, so jump to keys19 to skip the \ whole brake/throttle section BIT configJoystick \ If bit 7 of configJoystick is clear then the joystick BPL keys15 \ is not configured, so jump to keys15 to check for key \ presses for the brake/throttle LDX #2 \ Read the joystick y-axis into A and X, clearing the JSR GetADCChannel \ C flag if A < 10, setting A to the high byte, and \ setting X to 1 if the stick is up, or 0 if the stick \ is down BCC keys19 \ If A < 10 then the joystick is pretty close to the \ centre, so jump to keys19 so we don't register any \ throttle or brake activity STA T \ Set T = A / 2 LSR T ASL A \ Set A = A * 2 ADC T \ Set A = A + T \ = A * 2 + A / 2 \ = 2.5 * A BCS keys14 \ If the addition overflowed, then the joystick has \ moved a long way from the centre, so jump to keys14 to \ apply the brakes or throttle at full power CMP #250 \ If A < 250, jump to keys20 to store A in throttleBrake BCC keys20 \ and X in throttleBrakeState, so we store the amount of \ brakes or throttle to apply .keys14 CPX #0 \ If X = 0 then the joystick y-axis is positive (down), BEQ keys18 \ so jump to keys18 to apply the brakes BNE keys16 \ Otherwise the joystick y-axis is negative (up), so \ jump to keys16 to increase the throttle (this BNE is \ effectively a JMP as we already know X is non-zero)
Name: ProcessDrivingKeys (Part 4 of 6) [Show more] Type: Subroutine Category: Keyboard Summary: Process keyboard brake and throttle
Context: See this subroutine on its own page References: No direct references to this subroutine in this source file
.keys15 LDX #&AE \ Scan the keyboard to see if "S" is being pressed (the JSR ScanKeyboard \ throttle key) BNE keys17 \ If "S" is not being pressed, jump to keys17 to check \ the next key LDX #1 \ Set X = 1 to store in throttleBrakeState below .keys16 LDA #255 \ Set A = 255 to store in throttleBrake below BNE keys20 \ Jump to keys20 (this BNE is effectively a JMP as A is \ never zero) .keys17 LDX #&BE \ Scan the keyboard to see if "A" is being pressed (the JSR ScanKeyboard \ brake key) BNE keys19 \ If "A" is not being pressed, jump to keys19 LDX #0 \ Set X = 0 to store in throttleBrakeState below .keys18 LDA #250 \ Set A = 250 to store in throttleBrake below BNE keys20 \ Jump to keys20 (this BNE is effectively a JMP as A is \ never zero) .keys19 \ If we get here then neither of the throttle keys are \ being pressed, or the joystick isn't being moved \ enough to register a change LDX #%10000000 \ Set bit 7 of X to store in throttleBrakeState below LDA revCount \ Set A = 5 + revCount / 4 LSR A \ LSR A \ to store in throttleBrake below CLC ADC #5 .keys20 STX throttleBrakeState \ Store X in throttleBrakeState STA throttleBrake \ Store A in throttleBrake \ By the time we get here, one of the following is true: \ \ * There is no brake or throttle action from keyboard \ or joystick: \ \ throttleBrakeState = bit 7 set \ throttleBrake = 5 + revCount / 4 \ \ * Joystick is enabled and 2.5 * y-axis < 250 (so \ joystick is in the zone around the centre) \ \ throttleBrakeState = 0 for joystick down, brake \ 1 for joystick up, throttle \ throttleBrake = 2.5 * y-axis \ \ * "S" (throttle) is being pressed (or joystick has \ 2.5 * y-axis >= 250): \ \ throttleBrakeState = 1 \ throttleBrake = 255 \ \ * "A" (brake) is being pressed (or joystick has \ 2.5 * y-axis >= 250) \ \ throttleBrakeState = 0 \ throttleBrake = 250
Name: ProcessDrivingKeys (Part 5 of 6) [Show more] Type: Subroutine Category: Keyboard Summary: Process joystick gear change
Context: See this subroutine on its own page References: No direct references to this subroutine in this source file
BIT configJoystick \ If bit 7 of configJoystick is clear then the joystick BPL keys21 \ is not configured, so jump to keys21 to skip the \ following joystick-specific gear checks LDX #0 \ Call OSBYTE with A = 128 and X = 0 to fetch the ADC LDA #128 \ channel that was last used for ADC conversion, JSR OSBYTE \ returning the channel number in Y, and the status of \ the two fire buttons in X TXA \ If bit 0 of X is zero, then no fire buttons are being AND #1 \ pressed, so jump to keys22 to check the next key BEQ keys22 LDY throttleBrakeState \ If throttleBrakeState <> 1, then the throttle is not DEY \ being applied, so jump to keys23 BNE keys23 \ If we get here then the fire button is being pressed \ and the throttle is being applied, which is the \ joystick method for changing gear, so now we jump to \ the correct part below to change up or down a gear LDA throttleBrake \ If the throttle amount is >= 200, jump to keys24 to CMP #200 \ change up a gear BCS keys24 BCC keys23 \ Otherwise jump to keys23 to change down a gear
Name: ProcessDrivingKeys (Part 6 of 6) [Show more] Type: Subroutine Category: Keyboard Summary: Process keyboard gear change
Context: See this subroutine on its own page References: No direct references to this subroutine in this source file
.keys21 LDX #&9F \ Scan the keyboard to see if TAB is being pressed JSR ScanKeyboard BEQ keys23 \ If TAB is being pressed, jump to keys23 LDX #&EF \ Scan the keyboard to see if "Q" is being pressed JSR ScanKeyboard BEQ keys24 \ If "Q" is being pressed, jump to keys24 .keys22 \ If we get here then the gear is not being changed LDA #0 \ Set gearChange = 0 to indicate that we are not in the STA gearChange \ process of changing gear BEQ keys28 \ Jump to keys28 to return from the subroutine (this BEQ \ is effectively a JMP as A is always zero) .keys23 \ If we get here then either TAB is being pressed, or \ the joystick fire button is being pressed while the \ stick is in the "change down" part of the joystick \ range, so we need to change down a gear LDA #&FF \ Set A = -1 so we change down a gear BNE keys25 \ Jump to keys25 to change the gear .keys24 \ If we get here then either "Q" is being pressed, or \ the joystick fire button is being pressed while the \ stick is in the "change up" part of the joystick \ range, so we need to change up a gear LDA #1 \ Set A = 1 so we change up a gear .keys25 DEC gearChangeKey \ Set bit 7 of gearChangeKey (as we set gearChangeKey to \ zero above) LDX gearChange \ If gearChange is non-zero then we are already changing BNE keys28 \ gear, so jump to keys28 to return from the subroutine STA gearChange \ Set gearChange to -1 or 1 CLC \ Add A to the current gearNumber to get the number of ADC gearNumber \ the new gear, after the change CMP #&FF \ If the gear change will result in a gear of -1, jump BEQ keys26 \ to keys26 to set the gear number to 0 (the lowest gear \ number) CMP #7 \ If the new gear is not 7, jump to keys27 to change to BNE keys27 \ this gear LDA #6 \ Otherwise set A to 6, which is the highest gear number BNE keys27 \ allowed, and jump to keys27 to set this as the new \ gear number (this BNE is effectively a JMP as A is \ never zero) .keys26 LDA #0 \ If we get here then we just tried to change down too \ far, so set the number of the new gear to zero .keys27 STA gearNumber \ Store the new gear number in gearNumber JSR PrintGearNumber \ Print the new gear number on the gear stick .keys28 RTS \ Return from the subroutine