Skip to navigation

Revs on the BBC Micro

Revs G source

Name: MultiplyCoords [Show more] Type: Subroutine Category: Maths (Arithmetic) Summary: Multiply a 16-bit coordinate value and a 16-bit factor, optionally tallying or changing the sign of the result
Context: See this subroutine on its own page References: This subroutine is called as follows: * RotateVector calls MultiplyCoords * ApplySteeringForce calls entry point MultiplyCoords+7 * ApplySteeringSpeed calls entry point MultiplyCoords+7

This routine multiplies two 16-bit values and stores the result, optionally negating the result, or adding it to the existing contents of the destination. The first number (specified by parameter N, so let's call it variableN) is a 16-bit signed integer, while the second number (specified by parameter X, so let's call it variableX) is a 16-bit sign-magnitude number with the sign in bit 0 of the low byte. The result is stored in the variable specified by parameter K, so let's call it variableK. The values of bits 6 and 7 of A affect the result as follows: * If A = %00000000, then we calculate the following: variableK = variableN * variableX * If A = %01000000, then we calculate the following: variableK = variableK + variableN * variableX * If A = %10000000, then we calculate the following: variableK = - variableN * variableX * If A = %11000000, then we calculate the following: variableK = variableK - variableN * variableX Arguments: N Offset of the 16-bit signed number to multiply: * 0 = xPlayerSpeed * 1 = zPlayerSpeed * 6 = xPlayerAccel * 7 = zPlayerAccel * 8 = xVelocity * 9 = zVelocity * 10 = xTyreForceNose * 12 = zTyreForceNose X Offset of the 16-bit sign-magnitude value to multiply: * 0 = sinYawAngle * 1 = cosYawAngle * 2 = (steeringHi steeringLo) K Offset of the variable to store the result in: * 3 = xAcceleration * 4 = zAcceleration * 8 = xVelocity * 9 = zVelocity * 12 = zTyreForceNose * 14 = xSteeringForce A Details of the operation to perform: * Bit 6 defines whether we add or store: * 0 = store the result in the variable defined by K, overwriting the existing contents * 1 = add the result to the variable defined by K * Bit 7 defines the sign to apply to the result: * 0 = do not negate the multiplication * 1 = negate the multiplication Other entry points: MultiplyCoords+7 Use the following variables instead of the above: * Y = Offset of the 16-bit signed number to multiply (instead of N) * A = Offset of the variable to store the result in (instead of K) * H = Details of the operation to perform (instead of A)
.MultiplyCoords LDY N \ Set Y to the offset of the 16-bit signed number STA H \ Store the details of the operation to perform in H JMP mcoo1 \ Jump to mcoo1 to skip the MultiplyCoords+7 entry \ point \ This is where we join the subroutine when called via \ MultiplyCoords+7 STA K \ Set K to the offset of the variable to store the \ result in .mcoo1 LDA xPlayerSpeedHi,Y \ Set (QQ PP) to the 16-bit signed number pointed to by STA PP \ Y (variableY) LDA xPlayerSpeedTop,Y STA QQ LDA sinYawAngleLo,X \ Set (SS RR) to the 16-bit sign-magnitude number STA RR \ pointed to by X (variableX) LDA sinYawAngleHi,X STA SS JSR Multiply16x16 \ Set (A T) = (QQ PP) * (SS RR) \ \ And apply the sign from bit 7 of H STA U \ Set (U T) = (A T) \ = (QQ PP) * (SS RR) LDY K \ Set Y to K, so we can store the result in the variable \ pointed to by K BIT H \ If bit 6 of H is set, then jump to AddCoords to add BVS AddCoords \ the result to the variable pointed to by K LDA T \ Store the result in the variable pointed to by K STA xPlayerSpeedHi,Y LDA U STA xPlayerSpeedTop,Y RTS \ Return from the subroutine
Name: SubtractCoords [Show more] Type: Subroutine Category: Maths (Geometry) Summary: Subtract from a specified coordinate variable
Context: See this subroutine on its own page References: This subroutine is called as follows: * ApplyWingBalance calls SubtractCoords

This routine subtracts (U T) from the specified 16-bit variable. Specifically, it calculates: variableY = variableY - (U T) * abs(A) where A is the last variable to be loaded before the subroutine call. So if the call follows an LDA instruction, for example, the following is calculated if A is positive: variableY = variableY - (U T) and the following is calculated if A is negative: variableY = variableY + (U T) Arguments: N flag Determines the action: * If positive, subtract (U T) * If negative, add (U T)
.SubtractCoords BMI AddCoords \ If A is negative, jump to AddCoords to calculate: \ \ variableY = variableY + (U T) JSR Negate16Bit+2 \ Set (A T) = -(U T) STA U \ Set (U T) = (A T) \ = -(U T) \ Fall through into AddCoords to calculate: \ \ variableY = variableY - (U T)
Name: AddCoords [Show more] Type: Subroutine Category: Maths (Geometry) Summary: Add to a specified coordinate variable
Context: See this subroutine on its own page References: This subroutine is called as follows: * MultiplyCoords calls AddCoords * SubtractCoords calls AddCoords

This routine adds (U T) to the specified 16-bit variable. Arguments: Y Offset of the variable to update: * 3 = xAcceleration * 4 = zAcceleration * 6 = xPlayerAccel * 9 = zVelocity * 12 = zTyreForceNose * 14 = xSteeringForce
.AddCoords LDA xPlayerSpeedHi,Y \ Add (U T) to (xPlayerSpeedTop xPlayerSpeedHi) CLC \ ADC T \ starting with the low bytes STA xPlayerSpeedHi,Y LDA xPlayerSpeedTop,Y \ And then the high bytes ADC U STA xPlayerSpeedTop,Y RTS \ Return from the subroutine
Name: RotateCoordToCar [Show more] Type: Subroutine Category: Maths (Geometry) Summary: Rotate a vector from the 3D world coordinate system into the frame of reference of the player's car Deep dive: The core driving model
Context: See this subroutine on its own page References: This subroutine is called as follows: * ApplyDrivingModel calls RotateCoordToCar

Calculate the following: [ xVelocity ] [ cosYawAngle 0 -sinYawAngle ] [ xPlayerSpeed ] [ - ] = [ 0 1 0 ] . [ yPlayerSpeed ] [ zVelocity ] [ sinYawAngle 0 cosYawAngle ] [ zPlayerSpeed ] This rotates the player's delta vector from the 3D world coordinate system to the frame of reference of the player's car. The individual calculations are as follows: xVelocity = xPlayerSpeed * cosYawAngle - zPlayerSpeed * sinYawAngle zVelocity = xPlayerSpeed * sinYawAngle + zPlayerSpeed * cosYawAngle
.RotateCoordToCar LDY #0 \ Set Y = 0, so in the call to RotateVector, variableY \ is xPlayerSpeed and variableY+1 is zPlayerSpeed LDA #8 \ Set A = 8, so in the call to RotateVector, we store \ the result in xVelocity and zVelocity LDX #%11000000 \ Set bits 6 and 7 of X, to set the polarity in the call \ to RotateVector BNE RotateVector \ Jump to RotateVector to calculate the following: \ \ xVelocity = xPlayerSpeed * cosYawAngle \ - zPlayerSpeed * sinYawAngle \ \ zVelocity = xPlayerSpeed * sinYawAngle \ + zPlayerSpeed * cosYawAngle \ \ This BNE is effectively a JMP as X is never zero
Name: RotateCarToCoord [Show more] Type: Subroutine Category: Maths (Geometry) Summary: Rotate a vector from the frame of reference of the player's car into the 3D world coordinate system Deep dive: The core driving model
Context: See this subroutine on its own page References: This subroutine is called as follows: * ApplyDrivingModel calls RotateCarToCoord

Calculate the following: [ xAcceleration ] [ cosYawAngle 0 sinYawAngle ] [ xPlayerAccel ] [ - ] = [ 0 1 0 ] . [ - ] [ zAcceleration ] [ -sinYawAngle 0 cosYawAngle ] [ zPlayerAccel ] This rotates the xPlayerAccel vector from the frame of reference of the player's car into the 3D world coordinate system. The rotation matrix is the transpose of the matrix from RotateCoordToCar, which is the inverse, so the RotateCarToCoord routine reverses the rotation in the RotateCoordToCar routine. The individual calculations are as follows: xAcceleration = xPlayerAccel * cosYawAngle + zPlayerAccel * sinYawAngle zAcceleration = zPlayerAccel * cosYawAngle - xPlayerAccel * sinYawAngle
.RotateCarToCoord LDY #6 \ Set Y = 6, so in the call to RotateVector, variableY \ is xPlayerAccel and variableY+1 is zPlayerAccel LDA #3 \ Set A = 3, so in the call to RotateVector, we store \ the result in xAcceleration and zAcceleration LDX #%01000000 \ Set bit 6 and clear bit 7 of X, to set the polarity in \ the call to RotateVector \ Fall through into RotateVector to calculate the \ following: \ \ xAcceleration = xPlayerAccel * cosYawAngle \ + zPlayerAccel * sinYawAngle \ \ zAcceleration = zPlayerAccel * cosYawAngle \ - xPlayerAccel * sinYawAngle
Name: RotateVector [Show more] Type: Subroutine Category: Maths (Geometry) Summary: Rotate a vector by a rotation matrix
Context: See this subroutine on its own page References: This subroutine is called as follows: * RotateCoordToCar calls RotateVector

If bit 7 of X is clear, this routine calculates: [ variableA ] [ cosYawAngle 0 -sinYawAngle ] [ variableY ] [ - ] = [ 0 1 0 ] . [ - ] [ variableA+1 ] [ sinYawAngle 0 cosYawAngle ] [ variableY+1 ] by doing these individual calculations: variableA = variableY * cosYawAngle + variableY+1 * sinYawAngle variableA+1 = variableY+1 * cosYawAngle - variableY * sinYawAngle If bit 7 of X is set, this routine calculates: [ variableA ] [ cosYawAngle 0 sinYawAngle ] [ variableY ] [ - ] = [ 0 1 0 ] . [ - ] [ variableA+1 ] [ -sinYawAngle 0 cosYawAngle ] [ variableY+1 ] by doing these individual calculations: variableA = variableY * cosYawAngle - variableY+1 * sinYawAngle variableA+1 = variableY+1 * cosYawAngle + variableY * sinYawAngle For it to work, the routine must be called with bit 6 of X set. Arguments: Y Offset of the 16-bit signed number to multiply: * 0 = xPlayerSpeed and zPlayerSpeed * 6 = xPlayerAccel and zPlayerAccel A Offset of the variable to store the result in: * 3 = xAcceleration and zAcceleration * 8 = xVelocity and zVelocity X Details of the operation to perform on the second and fourth multiplications: * Bit 6 needs to be set * Bit 7 defines the sign to apply to the result: * 0 = do not negate the result * 1 = negate the result
.RotateVector STY N \ Set N to the offset of the 16-bit signed number, and \ let's call this number variableY (as it is specified \ by parameter Y) STA K \ Set K to the offset of the variable to store the \ result in, and let's call this number variableA (as it \ is specified by parameter A) STX GG \ Store the details of the operation to perform in GG LDX #1 \ Set X = 1, so in the call to MultiplyCoords we use \ cosYawAngle as the 16-bit sign-magnitude value to \ multiply LDA #%00000000 \ Set A = %00000000, so in the call to MultiplyCoords we \ overwrite the result rather than adding, and do not \ negate the multiplication JSR MultiplyCoords \ Set variableA = variableY * variableX \ = variableY * cosYawAngle DEX \ Set X = 0, so in the call to MultiplyCoords we use \ sinYawAngle as the 16-bit sign-magnitude value to \ multiply INC N \ Point to the next variable after the 16-bit signed \ number we just used, so in the call to MultiplyCoords \ we use variableY+1 as the 16-bit signed number LDA GG \ Set A to the details of the operation to perform in \ GG, as specified by parameter X \ \ Bit 6 of parameter X is always set in calls to this \ routine, so we add the result to variableA JSR MultiplyCoords \ Set: \ \ variableA = variableA + variableY+1 * variableX \ = variableA + variableY+1 * sinYawAngle \ \ if bit 7 of parameter X is clear, or: \ \ variableA = variableA - variableY+1 * sinYawAngle \ \ if bit 7 of parameter X is set INX \ Set X = 1, so in the call to MultiplyCoords we use \ cosYawAngle as the 16-bit sign-magnitude value to \ multiply INC K \ Point to the next variable after the one we just \ stored the result in, so in the call to MultiplyCoords \ we use variableA+1 to store the result LDA #%00000000 \ Set A = %00000000, so in the call to MultiplyCoords we \ overwrite the result rather than adding, and do not \ negate the multiplication JSR MultiplyCoords \ Set variableA+1 = variableY+1 * variableX \ = variableY+1 * cosYawAngle DEX \ Set X = 0, so in the call to MultiplyCoords we use \ sinYawAngle as the 16-bit sign-magnitude value to \ multiply DEC N \ Point back to the original variable for the 16-bit \ signed number we just used, so in the call to \ MultiplyCoords we use variableY again as the 16-bit \ signed number LDA GG \ Set A to the details of the operation to perform in EOR #%10000000 \ GG with bit 7 flipped, which is as specified by \ parameter X, but with a flipped sign \ \ Bit 6 of parameter X is always set in calls to this \ routine, so we add the result to variableA+1 JSR MultiplyCoords \ Set: \ \ variableA+1 = variableA+1 - variableY * variableX \ = variableA+1 - variableY * sinYawAngle \ \ if bit 7 of parameter X is clear, or: \ \ variableA+1 = variableA+1 + variableY * sinYawAngle \ \ if bit 7 of parameter X is set RTS \ Return from the subroutine
Name: ApplyDeltas [Show more] Type: Subroutine Category: Driving model Summary: Apply the deltas in the x-axis and z-axis to the player's coordinates Deep dive: The core driving model
Context: See this subroutine on its own page References: This subroutine is called as follows: * ApplyDrivingModel calls ApplyDeltas

Calculate the following: xPlayerCoord = xPlayerCoord + xPlayerSpeed * 2 zPlayerCoord = zPlayerCoord + zPlayerSpeed * 2 playerYawAngle = playerYawAngle + spinYawAngle
.ApplyDeltas LDX #1 \ Set X = 1, to act as an axis counter for xPlayerSpeed \ and zPlayerSpeed \ \ The comments below are for when X = 1 LDY #2 \ Set Y = 2, to act as a variable counter in the \ following loop, iterating through values 0 and 2, as \ Y is decremented twice at the end of the loop \ \ The comments below are for when Y = 2 .delt1 LDA #0 \ Set V = 0, to use as the sign bit for (V A T) STA V LDA xPlayerSpeedHi,X \ Set (V A T) = zPlayerSpeed STA T LDA xPlayerSpeedTop,X BPL delt2 \ If A is positive, jump to delt2 to skip the following \ instruction DEC V \ Set V = &FF, so it contains the correct sign bit for \ (V A T) .delt2 ASL T \ Set (U A T) = (V A T) << 1 ROL A ROL V STA U LDA xPlayerCoordLo,Y \ Set zPlayerCoord = zPlayerCoord + (U A T) ADC T \ = zPlayerCoord + zPlayerSpeed * 2 STA xPlayerCoordLo,Y \ \ starting with the low bytes LDA xPlayerCoordHi,Y \ Then the high bytes ADC U STA xPlayerCoordHi,Y LDA xPlayerCoordTop,Y \ And then the top bytes ADC V STA xPlayerCoordTop,Y DEY \ Decrement Y twice so the next iteration adds to the DEY \ xPlayerCoord variable DEX \ Decrement X so the next iteration sets (V A T) to \ xPlayerSpeed BPL delt1 \ Loop back until we have updated both xPlayerCoord and \ zPlayerCoord LDA playerYawAngleLo \ Set playerYawAngle = playerYawAngle + spinYawAngle CLC \ ADC spinYawAngleHi \ starting with the low bytes STA playerYawAngleLo LDA playerYawAngleHi \ And then the high bytes ADC spinYawAngleTop STA playerYawAngleHi RTS \ Return from the subroutine
Name: UpdateVelocity [Show more] Type: Subroutine Category: Driving model Summary: Update the player's velocity and spin yaw angle Deep dive: The core driving model
Context: See this subroutine on its own page References: This subroutine is called as follows: * ApplyDrivingModel calls UpdateVelocity

Calculate the following: xPlayerSpeed = xPlayerSpeed + xAcceleration << 5 zPlayerSpeed = zPlayerSpeed + zAcceleration << 3 spinYawAngle = spinYawAngle + spinYawDelta << 3
.UpdateVelocity LDX #2 \ Set X = 2, to act as an axis counter in the following \ loop, working through three axes from 2 to 0 .delf1 LDA #0 \ Set V = 0, to use as the sign bit for (V A T) STA V LDA xAccelerationLo,X \ Set (A T) = xAcceleration for axis X STA T LDA xAccelerationHi,X BPL delf2 \ If A is positive, jump to delf2 to skip the following \ instruction DEC V \ Set V = &FF, so it contains the correct sign bit for \ (V A T) .delf2 LDY #3 \ Set Y = 3, to act as a shift counter in the loop \ below, so we left-shift by three places CPX #2 \ If X <> 2, jump to delf3 to skip the following BNE delf3 \ instruction \ If we get here then X = 2, so (A T) contains \ spinYawDelta LDY #5 \ Set Y = 5, so for the third axis, we left-shift by \ five places .delf3 ASL T \ Set (V A T) = (V A T) << 1 ROL A ROL V DEY \ Decrement the shift counter in Y BNE delf3 \ Loop back until we have left-shifted Y times STA U \ Set (V U T) = (V A T) \ In the following, we update: \ \ * xPlayerSpeed when X = 0 \ \ * zPlayerSpeed when X = 1 \ \ * spinYawAngle when X = 2 \ \ The following comments are for when X = 0 LDA xPlayerSpeedLo,X \ Set xPlayerSpeed = xPlayerSpeed + (V U T) CLC \ ADC T \ starting with the low bytes STA xPlayerSpeedLo,X LDA xPlayerSpeedHi,X \ Then the high bytes ADC U STA xPlayerSpeedHi,X LDA xPlayerSpeedTop,X \ And then the top bytes ADC V STA xPlayerSpeedTop,X DEX \ Decrement the axis counter in X to point to the next \ axis BPL delf1 \ Loop back until we have processed all three axes RTS \ Return from the subroutine
Name: ProcessEngineStart [Show more] Type: Subroutine Category: Driving model Summary: Process the key press for starting the engine Deep dive: Modelling the engine
Context: See this subroutine on its own page References: This subroutine is called as follows: * ApplyEngine calls ProcessEngineStart
.ProcessEngineStart \ The routine is called from ApplyEngine when the engine \ is not running LDX #&DC \ Scan the keyboard to see if "T" is being pressed JSR ScanKeyboard BEQ engs2 \ If "T" is being pressed, jump to engs2 LDY gearNumber \ If gearNumber = 1, then we are in neutral, so jump to DEY \ engs1 BEQ engs1 \ If we get here then we are still in gear LDA playerSpeedHi \ Set A = playerSpeedHi BNE engs3 \ If A <> 0 then we are moving, so jump to engs3 to \ start the engine and set the rev counter to \ playerSpeedHi, as the engine can be restarted after \ stalling if we are going fast enough (i.e. if \ playerSpeedHi > 0) .engs1 \ If we get here then we are either in neutral, or we \ are not in neutral but are not moving LDA #0 \ Set A = 0 to set as the value of the rev counter BEQ SetRevsNoTorque \ Jump to SetRevsNoTorque to zero the rev counter and \ engine torque (this BEQ is effectively a JMP as A is \ always zero) .engs2 \ If we get here then then the engine is not running and \ "T" is being pressed 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 AND oddsOfEngineStart \ Set A = A mod oddsOfEngineStart BNE ThrobRevsNoTorque \ If A is non-zero, jump to ThrobRevsNoTorque \ Otherwise keep going to start the engine (which has a \ chance of 1 in oddsOfEngineStart of happening) and set \ the revs to zero (as A is zero) .engs3 \ If we get here then the engine is not running, and we \ are either already moving, or "T" is being pressed and \ we passed through the above (a 1 in oddsOfEngineStart \ chance) LDX #7 \ Set oddsOfEngineStart = 7, to reset it back to the STX oddsOfEngineStart \ default chance of starting the engine LDX #&FF \ Set engineStatus = &FF to turn on the engine STX engineStatus BMI ThrobRevsNoTorque \ Jump to ThrobRevsNoTorque to set the rev counter to A \ with a random throb added (this BMI is effectively a \ JMP as X is always negative)
Name: CalcRevsNoTorque [Show more] Type: Subroutine Category: Driving model Summary: Calculate the value of the rev counter according to the throttle being applied and zero the engine torque Deep dive: Modelling the engine
Context: See this subroutine on its own page References: This subroutine is called as follows: * ApplyEngine calls CalcRevsNoTorque * ApplyEngine calls entry point CalcRevsNoTorque-2

Other entry points: CalcRevsNoTorque-2 Set clutchEngaged to A before running the routine
STA clutchEngaged \ Set clutchEngaged = A .CalcRevsNoTorque LDA revCount \ Set A to the current rev counter LDX throttleBrakeState \ If throttleBrakeState <> 1 then the throttle is not DEX \ being applied, so jump to urev1 BNE urev1 ADC #7 \ Set A = A + 7 \ = revCount + 7 CMP throttleBrake \ If A >= throttleBrake, then the rev counter is higher BCS urev1 \ than the amount of throttle being applied, so jump to \ urev1 CMP #140 \ If A < 140, jump to SetRevsNoTorque to set the rev BCC SetRevsNoTorque \ counter to the new amount and zero the engine torque, \ returning from the subroutine using a tail call .urev1 CMP #42 \ If A < 42, jump to urev2 to set A = 40, the idling BCC urev2 \ level of the rev counter SEC \ Set A = A - 12 SBC #12 BCS ThrobRevsNoTorque \ Jump to ThrobRevsNoTorque to set the rev counter to \ the new amount, with a random throb added, and zero \ the engine torque, returning from the subroutine using \ a tail call (this BCS is effectively a JMP, as we know \ the subtraction won't underflow as A >= 42) .urev2 LDA #40 \ Set A = 40 to set as the rev count \ Fall through into ThrobRevsNoTorque to set the rev \ counter to 40, with a random throb added
Name: ThrobRevsNoTorque [Show more] Type: Subroutine Category: Driving model Summary: Set the rev counter after adding a random throb and zero the engine torque Deep dive: Modelling the engine
Context: See this subroutine on its own page References: This subroutine is called as follows: * CalcRevsNoTorque calls ThrobRevsNoTorque * ProcessEngineStart calls ThrobRevsNoTorque

Set the following: * revCount = A + rand(0-7) * revsOnGearChange = revCount * engineTorque = 0 * soundRevTarget = revTarget + 25
.ThrobRevsNoTorque STA T \ Store A in T 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 AND #7 \ Set A = A mod 8, which is a random number in the range \ 0 to 7 CLC \ Set A = A + T ADC T \ = random 0-7 + T \ Fall through into SetRevsNoTorque to set the rev \ counter to A and zero the engine torque
Name: SetRevsNoTorque [Show more] Type: Subroutine Category: Driving model Summary: Set the rev counter and zero the engine torque Deep dive: Modelling the engine
Context: See this subroutine on its own page References: This subroutine is called as follows: * CalcRevsNoTorque calls SetRevsNoTorque * ProcessEngineStart calls SetRevsNoTorque

Set the following: * revCount = A * revsOnGearChange = revCount * engineTorque = 0 * soundRevTarget = revTarget + 25
.SetRevsNoTorque STA revCount \ Set revCount = A STA revsOnGearChange \ Set revsOnGearChange = A \ Fall through into ZeroEngineTorque to zero \ engineTorque and set soundRevTarget
Name: ZeroEngineTorque [Show more] Type: Subroutine Category: Driving model Summary: Zero engineTorque Deep dive: Modelling the engine
Context: See this subroutine on its own page References: This subroutine is called as follows: * ApplyEngine calls ZeroEngineTorque

Set the following: * engineTorque = 0 * soundRevTarget = revTarget + 25
.ZeroEngineTorque LDA #0 \ Set A = 0 to set as the value of engineTorque JMP SetEngineTorque \ Jump to SetEngineTorque to set the following: \ \ engineTorque = 0 \ \ soundRevTarget = revTarget + 25 \ \ and return from the subroutine using a tail call
Name: ApplyEngine [Show more] Type: Subroutine Category: Driving model Summary: Apply the effects of the engine Deep dive: The core driving model Modelling the engine Matching the code to the driving model
Context: See this subroutine on its own page References: This subroutine is called as follows: * ApplyDrivingModel calls ApplyEngine

Calculate the following: * If the engine is not on, jump to ProcessEngineStart * If heightAboveTrack <> 0, jump to CalcRevsNoTorque to calculate the rev count and zero the engine torque * If a gear change key is being pressed, jump to CalcRevsNoTorque-2 to set bit 7 of clutchEngaged to indicate that the clutch is not engaged, calculate the rev count and zero the engine torque * If we are in neutral, jump to CalcRevsNoTorque to calculate the rev count and zero the engine torque * Otherwise, calculate the engine torque based on gear ratio, power and revs, setting the engineTorque, revCount and soundRevTarget variables as follows: * Set (A T) = trackGearRatio * playerSpeed * 8 * If clutch is not engaged: * If any of these are true: * Throttle is not being applied * playerSpeedHi >= 22 * This is a race and we are not showing the blue lights * This is a race, we are showing the blue lights and still have 10 iterations until the green * A >= revsOnGearChange, so the revs are higher than at the last gear change then engage the clutch, otherwise clutch stays disengaged and we do: A = revsOnGearChange If A >= 108, A = A - 2 and revsOnGearChange = revsOnGearChange - 2 * Set revCount = A * We now calculate the power being generated by the engine at this rev count. First, we cap the rev count to a maximum of 170: max(170, revCount) so in the following revCount is this capped number. * If revCount < 3, turn the engine off, zero the torque and quit * If 3 <= revCount < 83, set A = A * 2 + 152 * If 83 <= revCount < 87, set A = 186 - (revCount - 83) * If 87 <= revCount < 92, set A = 182 - ((revCount - 87) * 4) * If 92 <= revCount, set A = 162 - (revCount - 92) * 2 * Set engineTorque = trackGearPower * A * Set soundRevTarget = revCount + 25
.ApplyEngine LDA engineStatus \ If the engine is not on, jump to ProcessEngineStart to BEQ ProcessEngineStart \ process the key press for starting the engine, \ returning from the subroutine using a tail call LDA heightAboveTrack \ If heightAboveTrack <> 0, jump to CalcRevsNoTorque to BNE CalcRevsNoTorque \ calculate the revs and zero the engine torque, \ returning from the subroutine using a tail call LDA gearChangeKey \ If bit 7 of gearChangeKey is set then a gear change BMI CalcRevsNoTorque-2 \ key is being pressed, so jump to CalcRevsNoTorque-2 to \ set bit 7 of clutchEngaged to indicate that the clutch \ is not engaged, calculate the revs and zero the engine \ torque, returning from the subroutine using a tail \ call LDY gearNumber \ If gearNumber = 1, then we are in neutral, so jump to DEY \ CalcRevsNoTorque, returning from the subroutine using BEQ CalcRevsNoTorque \ a tail call \ If we get here then the engine is engaged and the car \ is on the track, so we need to calculate the engine \ torque and revs LDA playerSpeedLo \ Set (A T) = (playerSpeedHi playerSpeedLo) STA T LDA playerSpeedHi ASL T \ Set (A T) = (A T) << 1 ROL A PHP \ Store the flags on the stack, so we can retrieve it \ below BMI engi1 \ If bit 7 of (A T) is set, jump to engi1 to skip the \ following ASL T \ Set (A T) = (A T) << 1 ROL A \ \ so we only do this shift if we won't lose a bit off \ the left end of A .engi1 STA U \ Set U to the high byte of (A T), i.e. to the high byte \ of either speed * 2 or speed * 4 \ \ As the high byte in playerSpeedHi contains the speed \ in mph and the low byte contains the fraction, this \ just discards the fractional part of the calculation LDX gearNumber \ Set A to the gear ratio for the current gear, which is LDA trackGearRatio,X \ defined in the track data file at trackGearRatio JSR Multiply8x8 \ Set (A T) = A * U \ = trackGearRatio * high byte of (A T) \ = trackGearRatio * speed * 2 \ or trackGearRatio * speed * 4 ASL T \ Set (A T) = (A T) << 1 ROL A \ = trackGearRatio * speed * 4 \ trackGearRatio * speed * 8 PLP \ Retrieve the flags we stored on the stack above, and BPL engi2 \ if bit 7 is clear, skip the following, as we already \ doubled the result above ASL T \ Set (A T) = (A T) << 1 ROL A \ \ So we only do this shift if we didn't do it above, \ which means we have the following result, calculated \ in one of two ways to preserve accuracy: \ \ (A T) = trackGearRatio * playerSpeed * 8 .engi2 BIT clutchEngaged \ If bit 7 of clutchEngaged is clear then the clutch is BPL engi6 \ engaged, so jump to engi6 LDY throttleBrakeState \ If throttleBrakeState <> 1 then the throttle is not DEY \ being applied, so jump to engi4 to engage the clutch BNE engi4 LDY playerSpeedHi \ If playerSpeedHi >= 22, jump to engi4 to engage the CPY #22 \ clutch BCS engi4 LDY raceStarting \ Set Y = raceStarting BPL engi3 \ If bit 7 of raceStarting is clear, then we are not on \ the grid at the start of a race, so jump to engi3 to \ check the revs to decide if the clutch has engaged CPY #160 \ If raceStarting <> 160, then we are not showing the BNE engi4 \ blue lights at the start of the race, so jump to engi4 \ to engage the clutch \ If we get here then we are showing the blue lights at \ the start of the race, which we keep showing until the \ main loop counter is a multiple of 64 (at which point \ we show the green lights) PHA \ Store A on the stack to we can retrieve it below LDA mainLoopCounterLo \ If mainLoopCounterLo mod 64 < 53, clear the C flag AND #63 CMP #53 PLA \ Retrieve the value of A we stored on the stack above BCC engi4 \ If mainLoopCounterLo mod 64 < 53, then we have at \ least 63 - 53 = 10 main loop iterations (including \ this one) to go until the green lights appear, so jump \ to engi4 to engage the clutch .engi3 \ If we get here then we are either not on the starting \ grid, or we are on the starting grid, the blue lights \ are showing and we have fewer than 10 main loop \ iterations to go until the lights turn green \ Above, we set (A T) = trackGearRatio * speed * 8 CMP revsOnGearChange \ If A < revsOnGearChange, jump to engi5 to process the BCC engi5 \ clutch being disengaged \ Otherwise A >= revsOnGearChange, so the revs are \ higher than at the last gear change, so fall through \ into engi4 to engage the clutch .engi4 \ If we get here then we engage the clutch LDY #0 \ Set clutchEngaged = 0 to indicate that the clutch is STY clutchEngaged \ engaged BEQ engi6 \ Jump to engi6 (this BEQ is effectively a JMP as Y is \ always zero) .engi5 \ If we get here then the clutch is not engaged LDA revsOnGearChange \ Set A = revsOnGearChange CMP #108 \ If A < 108, i.e. revsOnGearChange < 108, jump to engi6 BCC engi6 \ to set the revs to this amount \ If we get here then revsOnGearChange >= 108 SEC \ Set revsOnGearChange = revsOnGearChange - 2 SBC #2 \ STA revsOnGearChange \ And fall through into engi6 to set revCount to the new \ value of revsOnGearChange .engi6 STA revCount \ Set revCount = A CMP #170 \ If A < 170, jump to engi7 to skip the following BCC engi7 \ instruction LDA #170 \ Set A = 170, so A has a maximum value of 170: \ \ A = max(170, revCount) .engi7 CMP #3 \ If A >= 3, jump to engi8 BCS engi8 INC engineStatus \ The rev count has fallen below 3, so increment \ engineStatus from &FF to 0, which turns the engine off JMP ZeroEngineTorque \ Jump to ZeroEngineTorque to set the engine torque to \ zero, returning from the subroutine using a tail call .engi8 SEC \ Set A = A - 66 SBC #66 \ = revCount - 66 BMI engi9 \ If A is negative (i.e. revCount < 66), jump to engi9 CMP #17 \ If A >= 17, jump to engi10 BCS engi10 .engi9 \ If we get here then either A < 0 or A < 17, so \ 3 <= revCount < 83 ASL A \ Set A = A * 2 + 152 CLC ADC #152 JMP engi13 \ Jump to engi13 .engi10 \ If we get here then A >= 0 and A >= 17, so \ revCount >= 83 SEC \ Set A = A - 17 SBC #17 \ = revCount - 66 - 17 \ = revCount - 83 CMP #4 \ If A >= 4 (i.e. revCount >= 87), jump to engi11 BCS engi11 \ If we get here then A is in the range 0 to 3 and \ 83 <= revCount < 87 EOR #&FF \ Set A = ~A, so A is in the range 255 to 252 CLC \ Set A = A + 187 ADC #187 \ = ~A + 187 \ = -A - 1 + 187 \ = 186 - A \ = 186 - (revCount - 83) BCS engi13 \ Jump to engi13 (the BCS is effectively a JMP as the \ above addition will always overflow) .engi11 \ If we get here then A >= 4, so revCount >= 87 SEC \ Set A = A - 4 SBC #4 \ = revCount - 83 - 4 \ = revCount - 87 CMP #5 \ If A >= 5, (i.e. revCount >= 92), jump to engi12 BCS engi12 \ If we get here then A is in the range 0 to 5 ASL A \ A = ~(A * 4) ASL A EOR #&FF CLC \ Set A = A + 183 ADC #183 \ = ~(A * 4) + 183 \ = -(A * 4) - 1 + 183 \ = 182 - (A * 4) \ = 182 - ((revCount - 87) * 4) BCS engi13 \ Jump to engi13 (the BCS is effectively a JMP as the \ above addition will always overflow) .engi12 \ If we get here then A >= 5, so revCount >= 92 SEC \ Set A = ~((A - 5) * 2) SBC #5 ASL A EOR #&FF CLC \ Set A = A + 163 ADC #163 \ = ~((A - 5) * 2) + 163 \ = -((A - 5) * 2) - 1 + 163 \ = 162 - (A - 5) * 2 \ = 162 - ((revCount - 87) - 5) * 2 \ = 162 - (revCount - 92) * 2 .engi13 STA U \ Set U to A LDA trackGearPower,X \ Set A to the gear power for the current gear, which is \ defined in the track data file at trackGearPower JSR Multiply8x8 \ Set (A T) = A * U \ = trackGearPower * U \ Fall through into SetEngineTorque to set the engine \ torque to the high byte in A
Name: SetEngineTorque [Show more] Type: Subroutine Category: Driving model Summary: Set engineTorque and soundRevTarget Deep dive: Modelling the engine
Context: See this subroutine on its own page References: This subroutine is called as follows: * ZeroEngineTorque calls SetEngineTorque

Set the following: * engineTorque = A * soundRevTarget = revTarget + 25
.SetEngineTorque STA engineTorque \ Set engineTorque = A LDA revCount \ Set soundRevTarget = revCount + 25 CLC ADC #25 STA soundRevTarget RTS \ Return from the subroutine
Name: ApplyTyreForces [Show more] Type: Subroutine Category: Driving model Summary: Calculate the tyre forces on the car Deep dive: The core driving model Skidding Matching the code to the driving model
Context: See this subroutine on its own page References: This subroutine is called as follows: * ApplyTyresAndSkids calls ApplyTyreForces

Calculate the following: * If xVelocity is non-zero and xVelocity and xTyreForceNose for tyre X have the same sign: * Set the high byte of xTyreForceNose = -xVelocity * 2^5 * Rotate a 1 into bit 7 of tyreSqueal for tyre X and finish * xTyreForceNose = -xVelocity * 2^5 * Call GetTyreForces to set (A T), H and (NN MM) * If the throttle is being applied and we are processing the front tyres, set: zTyreForceNose = 0 A = |xTyreForceNoseHi| otherwise: * If the throttle is being applied, then set: zTyreForceNose or zTyreForceRear = (A T) * abs(H) otherwise set: zTyreForceNose or zTyreForceRear = max((A T), (NN MM)) * abs(H) * Set the following (as appropriate for tyre X): A = max(|xTyreForceNoseHi|, |zTyreForceNoseHi|) + min(|xTyreForceNoseHi|, |zTyreForceNoseHi|) / 2 * Rotate a new bit 7 into tyreSqueal for tyre X as follows: * Clear if A <= wingForce for tyre X * Set if A > wingForce for tyre X Arguments: X The set of tyres to process: * 0 = front tyres * 1 = rear tyres
.ApplyTyreForces LDA xVelocityLo \ Set T = xVelocityLo STA T ORA xVelocityHi \ Set A = xVelocityLo OR xVelocityHi and store the flags PHP \ on the stack, which will be zero if both the high and \ low bytes of xVelocity are zero, i.e. if xVelocity = 0 LDA xVelocityHi \ Set (A T) = (xVelocityHi xVelocityLo) JSR Negate16Bit \ Set (A T) = -(A T) \ = -xVelocity LDY #5 \ Set Y = 5 to act as a shift counter in the following \ loop .tfor1 ASL T \ Set (A T) = (A T) << 1 ROL A DEY \ Decrement the shift counter BNE tfor1 \ Loop back until we have shifted left by five places, \ so we now have: \ \ (A T) = (A T) * 2^5 \ = -xVelocity * 2^5 STA xTyreForceNoseHi,X \ Set xTyreForceNoseHi for tyre X to the high byte of \ the result PLP \ Retrieve the flags that we stored on the stack, which \ will be zero if xVelocity = 0 BEQ tfor2 \ If xVelocity = 0, jump to tfor2 to set \ xTyreForceNoseLo to the low byte of the result \ If we get here then xVelocity is non-zero EOR xVelocityHi \ If xVelocityHi and xTyreForceNoseHi for tyre X have \ the same sign, this will clear bit 7 of A SEC \ Set the C flag BPL tfor7 \ If bit 7 of A is clear, then xVelocityHi and \ xTyreForceNoseHi for tyre X have the same sign, so \ jump to tfor7 with the C flag set to rotate a 1 into \ bit 7 of tyreSqueal for tyre X .tfor2 \ If we get here then either xVelocity = 0, or xVelocity \ is non-zero and xVelocity and xTyreForceNose have \ different signs LDA T \ Set xTyreForceNoseLo = T, so we now have: STA xTyreForceNoseLo,X \ \ xTyreForceNose = (A T) \ = -xVelocity * 2^5 JSR GetTyreForces \ Calculate the tyre forces due to the throttle or \ brakes: \ \ * (A T) = the force \ \ * H = the sign of the force \ \ * G = X + 2, so the call to ApplyLimitThrottle sets \ zTyreForceNose or zTyreForceRear accordingly \ \ * If the throttle is not being applied, (NN MM) is \ a maximum value for the force \ \ If the throttle is being applied and we are processing \ the front tyres, only G is calculated and the C flag \ is set BCC tfor3 \ If the C flag is clear then GetTyreForces successfully \ returned a set of calculated values, so jump to tfor3 \ to keep going LDA #0 \ Set zTyreForceNose = 0 STA zTyreForceNoseLo STA zTyreForceNoseHi LDA xTyreForceNoseHi \ Set A = xTyreForceNoseHi JSR Absolute8Bit \ Set A = |A| \ = |xTyreForceNoseHi| JMP tfor6 \ Jump to tfor6 .tfor3 JSR ApplyLimitThrottle \ If the throttle is being applied, then set \ zTyreForceNose or zTyreForceRear to: \ \ (A T) * abs(H) \ \ otherwise set it to: \ \ max((A T), (NN MM)) * abs(H) LDA zTyreForceNoseHi,X \ Set A = zTyreForceNoseHi for tyre X JSR Absolute8Bit \ Set A = |A| \ = |zTyreForceNoseHi| STA T \ Set T = A \ = |zTyreForceNoseHi| LDA xTyreForceNoseHi,X \ Set A = xTyreForceNoseHi for tyre X JSR Absolute8Bit \ Set A = |A| \ = |xTyreForceNoseHi| CMP T \ If A < T then |xTyreForceNoseHi| < |zTyreForceNoseHi|, BCC tfor4 \ so jump to tfor4 to halve A \ If we get here then |xTyreForceNoseHi| >= \ |zTyreForceNoseHi|, so we halve T LSR T \ Set T = T >> 1 \ = |zTyreForceNoseHi| / 2 JMP tfor5 \ Jump to tfor5 .tfor4 LSR A \ Set A = A >> 1 \ = |xTyreForceNoseHi| / 2 .tfor5 CLC \ Set A = A + T ADC T \ \ So if |zTyreForceNoseHi| > |xTyreForceNoseHi|: \ \ A = |zTyreForceNoseHi| + |xTyreForceNoseHi| / 2 \ \ or if |xTyreForceNoseHi| >= |zTyreForceNoseHi|: \ \ A = |xTyreForceNoseHi| + |zTyreForceNoseHi| / 2 \ \ In other words: \ \ A = max(|xTyreForceNoseHi|, |zTyreForceNoseHi|) \ + min(|xTyreForceNoseHi|, |zTyreForceNoseHi|) / 2 .tfor6 CMP wingForce,X \ If A <> wingForce for tyre X, jump to tfor7 with the C BNE tfor7 \ flag set as follows: \ \ * Clear if A < wingForce \ \ * Set if A > wingForce CLC \ Clear the C flag for when A = wingForce, so in all we \ have the following for the C flag: \ \ * Clear if A <= wingForce \ \ * Set if A > wingForce .tfor7 ROR tyreSqueal,X \ Rotate the C flag into bit 7 of tyreSqueal for tyre X RTS \ Return from the subroutine
Name: ApplySkidForces [Show more] Type: Subroutine Category: Driving model Summary: Calculate the tyre forces when the car is skidding Deep dive: Skidding
Context: See this subroutine on its own page References: This subroutine is called as follows: * ApplyTyresAndSkids calls ApplySkidForces

Calculate the following: * Set xTyreForceNose or xTyreForceRear = max(wingForce95 << 8, scaled |xVelocity|) * abs(-xVelocity) * Call GetTyreForces to set (A T), H and (NN MM) * If the throttle is being applied and we are processing the front tyres, return from the subroutine * If A < wingForce95, then: * If the throttle is being applied, then zTyreForceNose or zTyreForceRear = (A T) * abs(H) otherwise set: zTyreForceNose or zTyreForceRear = max((A T), (NN MM)) * abs(H) * If A >= wingForce95, then: * Set (A T) = wingForce95 << 8 * If the throttle is being applied, then zTyreForceNose or zTyreForceRear = (A T) * abs(H) otherwise set: zTyreForceNose or zTyreForceRear = max((A T), (NN MM)) * abs(H) * If the throttle is being applied and we are processing the rear tyres, set: xTyreForceNose or xTyreForceRear = 0 Arguments: X The set of tyres to process: * 0 = front tyres * 1 = rear tyres
.ApplySkidForces LDA #0 \ Set zTyreForceNose for tyre X to 0 STA zTyreForceNoseHi,X STA zTyreForceNoseLo,X LDY #8 \ Set Y = 8 so the call to Scale16Bit scales xVelocity JSR Scale16Bit \ Scale up |xVelocity| by 2^5, capping the result at the \ maximum possible positive value of &7Fxx, and \ returning the result in (NN MM) LDA xVelocityHi \ Set H = xVelocityHi with the sign flipped, so that EOR #%10000000 \ abs(H) = abs(-xVelocity) STA H LDA #0 \ Set T = 0 STA T LDA wingForce95,X \ Set A to wingForce95 for tyre X STX G \ Set G to the tyre number in X so the ApplyLimitAndSign \ routine sets xTyreForceNose or xTyreForceRear \ accordingly JSR ApplyLimitAndSign \ Set xTyreForceNose or xTyreForceRear \ = max((A T), (NN MM)) * abs(H) \ \ = max(wingForce95 << 8, scaled |xVelocity|) \ * abs(-xVelocity) JSR GetTyreForces \ Calculate the tyre forces due to the throttle or \ brakes: \ \ * (A T) = the force \ \ * H = the sign of the force \ \ * G = X + 2, so the call to ApplyLimitThrottle sets \ zTyreForceNose or zTyreForceRear accordingly \ \ * If the throttle is not being applied, (NN MM) is \ a maximum value for the force \ \ If the throttle is being applied and we are processing \ the front tyres, only G is calculated and the C flag \ is set BCS skid2 \ If the C flag is set then GetTyreForces did not return \ any calculated values, so jump to skid2 to return from \ the subroutine CMP wingForce95,X \ If A < wingForce95 for tyre X, jump to skid1 BCC skid1 LDA #0 \ Set T = 0 STA T LDA wingForce95,X \ Set A to wingForce95 for tyre X, so \ A = min(A, wingForce95) JSR ApplyLimitThrottle \ If the throttle is being applied, then set \ zTyreForceNose or zTyreForceRear to: \ \ (A T) * abs(H) \ \ otherwise set it to: \ \ max((A T), (NN MM)) * abs(H) LDY throttleBrakeState \ If throttleBrakeState <> 1 then the throttle is not DEY \ being applied, so jump to skid2 BNE skid2 CPX #0 \ If X = 0, then we are processing the front tyres, so BEQ skid2 \ jump to skid2 \ If we get here then the throttle is being applied and \ we are processing the rear tyres LDA #0 \ Set xTyreForceNose for tyre X to 0 STA xTyreForceNoseHi,X STA xTyreForceNoseLo,X BEQ skid2 \ Jump to skid2 to return from the subroutine (this BEQ \ is effectively a JMP as A is always zero) .skid1 JSR ApplyLimitThrottle \ If the throttle is being applied, then set \ zTyreForceNose or zTyreForceRear to: \ \ (A T) * abs(H) \ \ otherwise set it to: \ \ max((A T), (NN MM)) * abs(H) .skid2 RTS \ Return from the subroutine
Name: ApplyLimitThrottle [Show more] Type: Subroutine Category: Driving model Summary: Apply a maximum limit to a 16-bit number, unless the throttle is being applied
Context: See this subroutine on its own page References: This subroutine is called as follows: * ApplySkidForces calls ApplyLimitThrottle * ApplyTyreForces calls ApplyLimitThrottle

If the throttle is being applied, then set: variableG = (A T) * abs(H) otherwise set: variableG = max((A T), (NN MM)) * abs(H) Arguments: G Offset of the variable to set: * 0 = xTyreForceNose * 1 = xTyreForceRear * 2 = zTyreForceNose * 3 = zTyreForceRear
.ApplyLimitThrottle LDY throttleBrakeState \ If throttleBrakeState = 1, then the throttle is being DEY \ applied, so jump to lims1 to skip applying the maximum BEQ lims1 \ value to variableG, so: \ \ variableG = (A T) * abs(H) \ Otherwise fall through into ApplyLimitAndSign to set: \ \ variableG = max((A T), (NN MM)) * abs(H)
Name: ApplyLimitAndSign [Show more] Type: Subroutine Category: Driving model Summary: Apply a maximum limit and a sign to a 16-bit number
Context: See this subroutine on its own page References: This subroutine is called as follows: * ApplySkidForces calls ApplyLimitAndSign

Set the variable specified in G (let's call it variableG) as follows: variableG = max((A T), (NN MM)) * abs(H) Arguments: G Offset of the variable to set: * 0 = xTyreForceNose * 1 = xTyreForceRear * 2 = zTyreForceNose * 3 = zTyreForceRear
.ApplyLimitAndSign CMP NN \ If A < NN, jump to lims1 to skip the following BCC lims1 LDA MM \ Set (A T) = (NN MM) STA T LDA NN .lims1 BIT H \ Set the N flag to the sign of H, so the call to \ Absolute16Bit sets the sign of (A T) to abs(H) JSR Absolute16Bit \ Set the sign of (A T) to match the sign bit in H LDY G \ Set Y to the offset in G STA xTyreForceNoseHi,Y \ Store (A T) LDA T STA xTyreForceNoseLo,Y RTS \ Return from the subroutine
Name: Scale16Bit [Show more] Type: Subroutine Category: Maths (Arithmetic) Summary: Scale up a 16-bit value by 2^5
Context: See this subroutine on its own page References: This subroutine is called as follows: * ApplySkidForces calls Scale16Bit * GetTyreForces calls Scale16Bit

Scale up a 16-bit value by 2^5, capping the result at the maximum possible positive value of &7Fxx, and ensuring that the result is positive. Arguments: Y The offset of the variable to scale * 8 = xVelocity * 9 = zVelocity Returns: (NN MM) The scaled value
.Scale16Bit LDA xPlayerSpeedHi,Y \ Set (A MM) to the variable pointed to by Y, which we STA MM \ call variableY LDA xPlayerSpeedTop,Y BPL scal1 \ If the top byte in A is positive, jump to scal1 to \ skip the following LDA #0 \ Negate (A MM), starting with the low bytes SEC SBC MM STA MM LDA #0 \ And then the high bytes, so we now have: SBC xPlayerSpeedTop,Y \ \ (A MM) = |variableY| .scal1 LDY #5 \ Set Y = 5, to act as a shift counter in the following \ loop .scal2 ASL MM \ Set (A MM) = (A MM) << 1 ROL A BMI scal4 \ If bit 7 of the top byte in A is set, jump to scal4 \ to stop shifting and set the top byte to the largest \ possible positive top byte DEY \ Decrement the shift counter BNE scal2 \ Loop back until we have left-shifted five times .scal3 STA NN \ Set (NN MM) = (A MM) to return as the result RTS \ Return from the subroutine .scal4 LDA #%01111111 \ Set A = %01111111 to act as the largest possible \ positive top byte BNE scal3 \ Jump to scal3 to return (NN MM)
Name: GetTyreForces [Show more] Type: Subroutine Category: Driving model Summary: Calculate the tyre forces due to the throttle or brakes Deep dive: The core driving model Skidding
Context: See this subroutine on its own page References: This subroutine is called as follows: * ApplySkidForces calls GetTyreForces * ApplyTyreForces calls GetTyreForces

Calculate the following: * If the throttle is not being applied: * (NN MM) = scaled up |zVelocity| * H = zVelocityHi with the sign flipped * (A T) = throttleBrake * wingForce (rear tyres) = throttleBrake * wingForce * 3 / 4 (front tyres) * C flag clear * If the throttle is being applied: * When we are processing the front tyres: * C flag set * When we are processing the rear tyres: * H = gearNumber - 1 * (A T) = (throttleBrake * engineTorque) / 2 * C flag clear The routine also sets G to 2 or 3 (for the front or rear tyres). Arguments: X The set of tyres to process: * 0 = front tyres * 1 = rear tyres Returns: C flag Calculation status: * Set if we are returning a set of calculated values * Clear if we are not returning any values (which happens if the throttle is being applied and we are processing the front tyres) G Set to the correct offset for calling ApplyLimitThrottle or ApplyLimitAndSign: * 2 = zTyreForceNose * 3 = zTyreForceRear H The sign of the force (A T) The force (NN MM) If the throttle is not being applied, a maximum for the force
.GetTyreForces TXA \ Set G = X + 2 CLC \ ADC #2 \ So G = 2 for the front tyres and 3 for the rear tyres STA G LDY throttleBrakeState \ If throttleBrakeState = 1 then the throttle is being DEY \ applied, so jump to tyfo1 BEQ tyfo1 \ If we get here then the throttle is not being applied LDY #9 \ Set Y = 8 so the call to Scale16Bit scales zVelocity JSR Scale16Bit \ Scale up |zVelocity| by 2^5, capping the result at the \ maximum possible positive value of &7Fxx, and \ returning the result in (NN MM) LDA zVelocityHi \ Set H = zVelocityHi with the sign flipped EOR #%10000000 STA H LDA wingForce,X \ Set A to wingForce for tyre X CPX #1 \ If X = 1, then we are processing the rear tyres, so BEQ tyfo2 \ jump to tyfo2 to use this value of A in the \ calculation LSR A \ Set A = (A / 2 + wingForce) / 2 CLC \ = (wingForce / 2 + wingForce) / 2 ADC wingForce,X \ = wingForce * 3 / 4 LSR A JMP tyfo2 \ Jump to tyfo2 to use this value of A in the \ calculation .tyfo1 \ If we get here then the throttle is being applied CPX #1 \ If X <> 1, then we are processing the front tyres, so BNE tyfo4 \ jump to tyfo4 to return from the subroutine with the C \ flag set LDA gearNumber \ Set H = gearNumber - 1 SEC SBC #1 STA H LDA engineTorque \ Set A = engineTorque .tyfo2 STA U \ Store the value of A in U to use in the multiplication \ below LDA throttleBrake \ Set A to the amount of throttle or brake being applied JSR Multiply8x8 \ Set (A T) = A * U \ = throttleBrake * U LDY throttleBrakeState \ If throttleBrakeState <> 1 then the throttle is not DEY \ being applied, so jump to tyfo3 BNE tyfo3 LSR A \ The throttle is being applied, so set: ROR T \ \ (A T) = (A T) / 2 .tyfo3 CLC \ Clear the C flag to indicate that we are returning a \ set of calculated values RTS \ Return from the subroutine .tyfo4 SEC \ Set the C flag to indicate that we are not returning \ any calculated values RTS \ Return from the subroutine
Name: ApplyGrassOrTrack [Show more] Type: Subroutine Category: Driving model Summary: Apply the effects of driving or braking on grass Deep dive: The core driving model Driving on grass Matching the code to the driving model
Context: See this subroutine on its own page References: This subroutine is called as follows: * ApplyDrivingModel calls ApplyGrassOrTrack

* If all the following are true: * There is grass under at least one side of the car * bumpyGrassHeight = 0 * heightAboveTrack = 0 * Bit 7 of playerDrift is set then calculate the following to make the car jump when it hits the verge: * yGravityDelta = playerSpeedHi / 2 * yJumpHeight = playerSpeedHi / 4 * heightAboveTrack = heightAboveTrack + 1 * spinYawAngleHi = spinYawAngleHi >> 1 with bit 7 set and make the crash/contact sound * If there is grass under at least one side of the car, then: * bumpyGrassHeight = random number in the range 1 to 7 that is higher with higher speeds otherwise: * bumpyGrassHeight = 0 * For each wing, calculate the following: * If the brakes are not being applied, set brakeForce = 0, otherwise set: * brakeForce = -zTyreForceBoth / 8 (front wing) * brakeForce = zTyreForceBoth / 8 (rear wing) * If we are driving on grass on both sides of the car, then set: wingForce = wingForceGrass + brakeForce otherwise calculate: wingForce = wingSetting * min(53, playerSpeedHi) * abs(zVelocity) + wingForceTrack + brakeForce * Set: wingForce95 = wingForce * 243 / 256
.ApplyGrassOrTrack LDA #0 \ Set A = 0 LDY throttleBrakeState \ If throttleBrakeState is non-zero, then the brakes are BNE gras1 \ not being applied, so jump to gras1 to set H = 0 and \ G = 0 \ If we get here then the brakes are being applied LDA zTyreForceBoth \ Set A = zTyreForceBoth PHP \ Store the N flag on the stack, so the PLP below sets \ the N flag according to the value of zTyreForceBoth LSR A \ Set A = A >> 3 LSR A \ = zTyreForceBoth / 8 LSR A PLP \ If zTyreForceBoth is positive, jump to gras1 to skip BPL gras1 \ the following instruction ORA #%11100000 \ Set bits 5-7 of A to ensure that the value of A \ retains the correct sign after being shifted \ By this point, A = zTyreForceBoth / 8 and has the \ correct sign .gras1 STA H \ Set H = A EOR #&FF \ Set G = -A CLC ADC #1 STA G \ So if the brakes are not being applied we have: \ \ * G = 0 \ * H = 0 \ \ and if the brakes are being applied, we have: \ \ * G = -zTyreForceBoth / 8 \ * H = zTyreForceBoth / 8 \ \ G is used in the front wing calculation below, while \ H is used in the rear wing calculation LDA playerSpeedHi \ Set U = playerSpeedHi STA U LDX #0 \ Set X = 0, to store as the value of bumpyGrassHeight \ if we are not driving on grass \ The leftSurface and rightSurface locations are in \ screen memory, and are just to the left or right of \ the dashboard, so: \ \ * If leftSurface = &FF then there is grass under the \ left edge of the dashboard \ \ * If rightSurface = &FF then there is grass under \ the right edge of the dashboard LDA leftSurface \ Set W so it is &FF if there is grass under both edges AND rightSurface \ of the dashboard STA W LDA leftSurface \ If leftSurface = &FF, then there is grass under the CMP #&FF \ left side of the car, so jump to gras2 BEQ gras2 LDA rightSurface \ If rightSurface <> &FF, then there isn't grass under CMP #&FF \ the right side of the car, so jump to gras4 to skip BNE gras4 \ the following and set bumpyGrassHeight to 0 .gras2 \ If we get here then there is grass under at least one \ side of the car, and W = &FF if there is grass under \ both sides 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 JSR Multiply8x8 \ Set (A T) = A * U \ = random * playerSpeedHi \ \ So A is a random number that is higher with higher \ speeds AND #7 \ Set X = A mod 8 TAX BNE gras3 \ If X = 0, set X = 1 INX \ So X is now a random number in the range 1 to 7 that \ is higher with higher speeds .gras3 LDA bumpyGrassHeight \ If bumpyGrassHeight is non-zero, jump to gras4 BNE gras4 LDA bumpyGrassHeight \ If heightAboveTrack is non-zero, jump to gras4 (the ORA heightAboveTrack \ LDA is not required here, so perhaps it's left over BNE gras4 \ from development) BIT playerDrift \ If bit 7 of playerDrift is clear, then the player's BPL gras4 \ car is not moving significantly sideways, so jump to \ gras4 \ If we get here then the player's car is drifting, and \ the car is not jumping on bumpy grass (as \ bumpyGrassHeight = 0) and is glued to the track (as \ heightAboveTrack = 0) JSR ApplyVergeJump \ Calculate the following variables to apply a jump to \ the car when we hit the track verge: \ \ yGravityDelta = playerSpeedHi / 2 \ \ yJumpHeight = playerSpeedHi / 4 \ \ heightAboveTrack = heightAboveTrack + 1 \ \ spinYawAngleHi = spinYawAngleHi >> 1 with bit 7 set \ \ and make the crash/contact sound .gras4 STX bumpyGrassHeight \ Set bumpyGrassHeight = X, so: \ \ * If we are not driving on grass, then \ bumpyGrassHeight = 0 \ \ * If we are driving on grass, then bumpyGrassHeight \ is set to a random number in the range 1 to 7 that \ is higher with higher speeds LDX #1 \ We now run the following loop, once for each wing \ setting, so set X to use as a loop counter, from 1 \ (for the front wing) to 0 (for the read wing) .gras5 LDA playerSpeedHi \ Set A = playerSpeedHi CMP #53 \ If A < 53, jump to gras6 BCC gras6 LDA #53 \ Set A = 53, so A has a maximum value of 53 .gras6 STA U \ Store A in U, so we now have the following: \ \ U = min(53, playerSpeedHi) LDA wingSetting,X \ Set A to the scaled wing setting for wing X JSR Multiply8x8 \ Set (A T) = A * U \ = wingSetting * min(53, playerSpeedHi) BIT zVelocityHi \ Set the N flag according to the sign in bit 7 of \ zVelocityHi, so the call to Absolute8Bit sets the \ sign of A to the same sign as zVelocity JSR Absolute8Bit \ Set A = A * abs(zVelocity) \ = wingSetting * min(53, playerSpeedHi) \ * abs(zVelocity) CLC \ Set A = A + wingForceTrack for this wing ADC wingForceTrack,X \ = wingSetting * min(53, playerSpeedHi) \ * abs(zVelocity) \ + wingForceTrack LDY #243 \ Set U = 243 to use in the 95% calculation below STY U LDY W \ If W <> &FF, then there is not grass under both sides CPY #&FF \ of the car, so jump to gras7 to skip the following BNE gras7 LDA wingForceGrass,X \ There is grass under both sides of the car, so set A \ to the wingForceGrass value for this wing LDY #&FF \ Set Y = &FF (this has no effect as Y is already &FF, \ so this instruction is a bit of a mystery) .gras7 CLC \ Set A = A + G (front wing) or H (rear wing) ADC G,X \ \ This adds the effect of the brakes being applied STA wingForce,X \ Store A in wingForce for this wing JSR Multiply8x8 \ Set (A T) = A * U \ = A * 243 \ \ so A = A * 243 / 256 \ = A * 0.95 STA wingForce95,X \ Store A in wingForce95 for this wing DEX \ Decrement the loop counter in X to point to the next \ wing BPL gras5 \ Loop back until we have processed both wings RTS \ Return from the subroutine
Name: wingForceTrack [Show more] Type: Variable Category: Driving model Summary: The base downward force from the weight of the car when on the track, to which the downward force from the wings is added Deep dive: The core driving model
Context: See this variable on its own page References: This variable is used as follows: * ApplyGrassOrTrack calls wingForceTrack
.wingForceTrack EQUB 53 \ Front wing EQUB 53 \ Rear wing
Name: wingForceGrass [Show more] Type: Variable Category: Driving model Summary: The base downward force from the weight of the car when on grass, to which the downward force from the wings is added Deep dive: The core driving model Driving on grass
Context: See this variable on its own page References: This variable is used as follows: * ApplyGrassOrTrack calls wingForceGrass
.wingForceGrass EQUB 25 \ Front wing EQUB 26 \ Rear wing
Name: ApplyWingBalance [Show more] Type: Subroutine Category: Driving model Summary: Apply the effect of the wing settings Deep dive: The core driving model Driving on grass
Context: See this subroutine on its own page References: This subroutine is called as follows: * ApplyDrivingModel calls ApplyWingBalance

This routine calcvulates the following: xPlayerAccel = xPlayerAccel - scaledSpeed * xPrevVelocityHi zPlayerAccel = zPlayerAccel - scaledSpeed * (wingBalance * playerSpeedHi + 2048) * abs(zVelocity) where scaledSpeed = playerSpeedHi if bumpyGrassHeight = 0 playerSpeedHi * 2 otherwise and wingBalance = 60 + (rearWingSetting * 3 + frontWingSetting) / 2
.ApplyWingBalance LDA xPrevVelocityHi \ Set A = xPrevVelocityHi JSR Absolute8Bit \ Set A = |A| \ = |xPrevVelocityHi| STA U \ Set U = A \ = |xPrevVelocityHi| CMP playerSpeedHi \ If A >= playerSpeedHi, jump to bala1 BCS bala1 LDA playerSpeedHi \ Set A = playerSpeedHi, so A has a minimum value of \ playerSpeedHi .bala1 LDY bumpyGrassHeight \ If bumpyGrassHeight = 0, jump to bala2 to skip the BEQ bala2 \ following ASL A \ Set A = A * 2 \ playerSpeedHi * 2 .bala2 STA W \ Set W = A \ = playerSpeedHi if bumpyGrassHeight = 0 \ playerSpeedHi * 2 otherwise \ \ Let's call this value scaledSpeed JSR Multiply8x8 \ Set (A T) = A * U \ = scaledSpeed * |xPrevVelocityHi| STA U \ Set (U T) = (A T) \ = scaledSpeed * |xPrevVelocityHi| LDY #6 \ Set Y = 6, so the call to SubtractCoords uses \ xPlayerAccel LDA xPrevVelocityHi \ Set A = xPrevVelocityHi so the call to SubtractCoords \ sets the sign to abs(xPrevVelocity) JSR SubtractCoords \ Set: \ \ variableY = variableY - (U T) * abs(A) \ \ so that's: \ \ xPlayerAccel = xPlayerAccel \ - scaledSpeed * |xPrevVelocityHi| \ * abs(xPrevVelocity) \ \ = xPlayerAccel - scaledSpeed * xPrevVelocityHi LDA playerSpeedHi \ Set U = playerSpeedHi STA U LDA wingBalance \ Set A = wingBalance, which is calculated as: \ \ 60 + (rearWingSetting * 3 + frontWingSetting) / 2 JSR Multiply8x8 \ Set (A T) = A * U \ = wingBalance * playerSpeedHi CLC \ Set V = A + 8 ADC #8 \ STA V \ so (V T) = (A T) + (8 0) \ = wingBalance * playerSpeedHi + 2048 LDA W \ Set U = W STA U \ = scaledSpeed JSR Multiply8x16 \ Set: \ \ (U T) = U * (V T) \ = scaledSpeed \ * (wingBalance * playerSpeedHi + 2048) LDY #7 \ Set Y = 6, so the call to SubtractCoords uses \ zPlayerAccel LDA zVelocityHi \ Set A = zVelocityHi so the call to SubtractCoords sets \ the sign to abs(zVelocity) JSR SubtractCoords \ Set: \ \ variableY = variableY - (U T) * abs(A) \ \ so that's: \ \ zPlayerAccel = zPlayerAccel - scaledSpeed \ * (wingBalance * playerSpeedHi + 2048) \ * abs(zVelocity) RTS \ Return from the subroutine
Name: BuildRoadSign [Show more] Type: Subroutine Category: 3D objects Summary: Create an object for the road sign Deep dive: Road signs
Context: See this subroutine on its own page References: This subroutine is called as follows: * MainDrivingLoop (Part 2 of 5) calls BuildRoadSign
.BuildRoadSign LDX currentPlayer \ Set X to the driver number of the current player LDY objTrackSection,X \ Set Y to the track section number * 8 for the current \ player LDA trackSectionData,Y \ Set A to bits 4-7 of the trackSectionData for the LSR A \ track section, shifted into bits 0-3, so A contains LSR A \ the number of the sign for this section (0 to 15) LSR A LSR A STA thisSignNumber \ Store the sign number in thisSignNumber, so we can \ retrieve it below CMP previousSignNumber \ If previousSignNumber doesn't already contain this BNE sign1 \ sign number, jump to sign1 to skip the following \ We get here if we are calling BuildRoadSign again for \ the same sign number (we set previousSignNumber to \ the current sign number later in the routine) \ \ This ensures that we display signs at the start of \ sections that have a different value in bits 4-7 of \ trackSectionData compared to the previous section, so \ signs appear when we move into a section with a new \ value in bits 4-7 of the trackSectionData (and that \ value is the number of the sign to show) ADC #0 \ Increment the sign number in A (we know the C flag is \ set, as the above comparison was equal) AND #15 \ Restrict the result to the range 0 to 15, i.e. set A \ to A mod 16 .sign1 TAX \ Set X to the sign number, which will either be the \ sign number from trackSectionData for this section, or \ the sign number plus 1 \ We now draw sign number X, by fetching the track sign \ vector, adding it to the track section coordinate to \ get the sign's 3D coordinates, and subtracting the \ player's coordinates to get the sign's vector relative \ to the player (i.e. relative to the camera) \ \ We then calculate the yaw and pitch angles and store \ them in object 23, so it can be drawn by the call to \ DrawCarOrSign from the main driving loop LDY #2 \ Set Y = 2, so the call to AddScaledVector scales by \ 2 ^ (8 - Y) = 2 ^ 6 STY W \ Set W = 2, which gets decremented through each call \ to AddScaledVector as we work through each axis, so it \ stores the results in each axis of xRoadSignCoord in \ turn LDA zTrackSignVector,X \ Set A to the 3D z-coordinate of the track sign vector JSR AddScaledVector \ Set zRoadSignCoord \ = zPlayerCoord - zTrackSignVector * 2 ^ 6 LDY #4 \ Set Y = 4, so the call to AddScaledVector scales by \ 2 ^ (8 - Y) = 2 ^ 4 LDA yTrackSignVector,X \ Set A to the 3D y-coordinate of the track sign vector JSR AddScaledVector \ Set yRoadSignCoord \ = yPlayerCoord - yTrackSignVector * 2 ^ 4 LDY #2 \ Set Y = 2, so the call to AddScaledVector scales by \ 2 ^ (8 - Y) = 2 ^ 6 LDA xTrackSignVector,X \ Set A to the 3D x-coordinate of the track sign vector JSR AddScaledVector \ Set xRoadSignCoord \ = xPlayerCoord - xTrackSignVector * 2 ^ 6 \ We now have: \ \ [ xRoadSignCoord ] [ xPlayerCoord ] \ [ yRoadSignCoord ] = [ yPlayerCoord ] \ [ zRoadSignCoord ] [ zPlayerCoord ] \ \ [ xTrackSignVector * 2 ^ 6 ] \ - [ yTrackSignVector * 2 ^ 4 ] \ [ zTrackSignVector * 2 ^ 6 ] LDA trackSignData,X \ Set objectType to the object type for road sign X, AND #%00000111 \ which comes from bits 0-2 of trackSignData, and CLC \ gets 7 added to get the range 7 to 12 ADC #7 STA objectType LDA trackSignData,X \ Set Y to the track section number for road sign X, AND #%11111000 \ which comes from bits 3-7 of trackSignData TAY LDX #&FD \ Set X = &FD so the calls to GetSectionCoord, \ GetObjYawAngle and GetObjPitchAngle use xCoord2, \ yCoord2 and zCoord2 JSR GetSectionCoord \ Copy the first trackSectionI coordinate for track \ section Y into xCoord2, so xCoord2 contains the 3D \ coordinates of the inside track for the start of this \ track section, i.e. \ \ [ xCoord2 ] [ xTrackSectionI ] \ [ yCoord2 ] = [ yTrackSectionI ] \ [ zCoord2 ] [ zTrackSectionI ] LDY #6 \ Set Y = 6 so the call to GetObjYawAngle uses \ xRoadSignCoord for the second variable, so we \ calculate the yaw and pitch angles for an object at \ the following 3D coordinates (if we just consider the \ the x-axis, for clarity): \ \ xCoord2 - xRoadSignCoord \ = xTrackSectionI - (xPlayerCoord - xTrackSignVector) \ = xTrackSectionI - xPlayerCoord + xTrackSignVector \ = xTrackSectionI + xTrackSignVector - xPlayerCoord \ \ The xPlayerCoord vector contains the 3D coordinates \ of the player's car, so the above gives us the vector \ from the player's car to the sign \ \ So this is the vector we use to calculate the object's \ angles, to show the sign in the correct place by the \ side of the track from the viewpoint of the player JSR GetObjYawAngle \ Calculate the object's yaw angle, returning it in \ (JJ II) LDA II \ Store the yaw angle from (JJ II) in (objYawAngleHi+23 STA objYawAngleLo+23 \ objYawAngleLo+23), so we use object 23 to store the LDA JJ \ sign object STA objYawAngleHi+23 \ Now that the sign object has been built and the angles \ calculated, we can check for any collisions between \ the player and the sign SEC \ Set A = JJ - playerYawAngleHi SBC playerYawAngleHi JSR Absolute8Bit \ Set A = |A| \ = |JJ - playerYawAngleHi| \ \ So A contains the screen distance between the player \ and the sign in the x-axis CMP #64 \ If A < 64, jump to sign2 to skip the following BCC sign2 LDY thisSignNumber \ Set previousSignNumber to the sign number from the STY previousSignNumber \ trackSectionData, so the next time we display a sign \ it will be the next sign .sign2 LDY #37 \ Set Y = 37 to use as the collision distance in the \ call to CheckForContact below CMP #110 \ If A < 110, jump to sign3 to skip the following BCC sign3 LDY #80 \ Set Y = 80 to use as the collision distance in the \ call to CheckForContact below .sign3 LDA #23 \ Set objectNumber = 23, so if we are colliding with the STA objectNumber \ sign, CheckForContact sets collisionDriver to object \ 23 (which is now the sign object) JSR CheckForContact \ Check to see if the object and the player's car are \ close enough for contact, specifically if they are \ within a distance of Y from each other \ The final step is to calculate the object's pitch \ angle, and then we are done building the sign object LDY #6 \ Set Y = 6 so the call to GetObjPitchAngle uses \ xRoadSignCoord JSR GetObjPitchAngle \ Calculate the object's pitch angle, returning it \ in A and LL \ \ If the object is not visible on-screen, the C flag is \ set, which will hide the object in the following call \ to SetObjectDetails JSR SetObjectDetails \ Set the object's visibility, scale and type RTS \ Return from the subroutine
Name: AddScaledVector [Show more] Type: Subroutine Category: Maths (Geometry) Summary: Add a scaled vector to another vector, one axis at a time
Context: See this subroutine on its own page References: This subroutine is called as follows: * BuildRoadSign calls AddScaledVector

This routine does the following calculation, one axis at a time (starting with the z-axis, then the y-axis and x-axis): [ xRoadSignCoord ] [ xPlayerCoord ] [ xTrackSignVector * 2 ^ (8 - Y) ] [ yRoadSignCoord ] = [ yPlayerCoord ] - [ yTrackSignVector * 2 ^ (8 - Y) ] [ zRoadSignCoord ] [ zPlayerCoord ] [ zTrackSignVector * 2 ^ (8 - Y) ] The value of Y can be varied between calls to change the scale factor on a per-axis basis. Arguments: A The relevant from xTrackSignVector, yTrackSignVector, zTrackSignVector W Set to 2 for the first call Y The scale factor for this axis
.AddScaledVector PHA \ Set (V A T) = (0 A 0) LDA #0 \ = A * 256 STA T STA V PLA BPL addv1 \ If A is positive then V is already the correct high \ byte for (V A T), so jump to addv1 DEC V \ Otherwise, decrement V to &FF so it's the correct \ high byte for (V A T) \ We now shift (V A T) right by Y places .addv1 LSR V \ Set (V A T) = (V A T) >> 1 ROR A ROR T DEY \ Decrement the shift counter BNE addv1 \ Loop back until we have right-shifted Y times STA U \ Set (V U T) = (V A T) \ = A * 256 >> Y \ = A * 2 ^ (8 - Y) \ \ We know that V doesn't contain any data, just sign \ bits, so this means: \ \ (U T) = A * 2 ^ (8 - Y) \ = xTrackSignVector * 2 ^ (8 - Y) LDY W \ Fetch the axis index from W DEC W \ Decrement the axis index in W, ready for the next call \ to AddScaledVector LDA xPlayerCoordHi,Y \ Set xRoadSignCoord = xPlayerCoord - (U T) SEC \ SBC T \ starting with the low bytes STA xRoadSignCoordLo,Y LDA xPlayerCoordTop,Y \ And then the high bytes SBC U STA xRoadSignCoordHi,Y RTS \ Return from the subroutine
Name: InitialiseDrivers [Show more] Type: Subroutine Category: Drivers Summary: Initialise all 20 drivers on the starting grid
Context: See this subroutine on its own page References: This subroutine is called as follows: * MainLoop (Part 1 of 6) calls InitialiseDrivers

Arguments: X The routine is always called with X = 0
.InitialiseDrivers STX setSpeedForDriver \ Set setSpeedForDriver = 0, to use as a loop counter \ when initialising all 20 drivers STX raceClass \ Set raceClass = 0 (Novice) JSR GetSectionSteering \ Set up the optimum steering for each section for a \ Novice race, storing the results in sectionSteering \ and returning with X unchanged \ The following loop works starts with X = 0, and then \ loops down from 19 to 1, working its way through each \ of the 20 drivers .driv1 TXA \ Set A to the current driver number in X STA driversInOrder,X \ Set driversInOrder for driver X to the driver number LSR A \ Set the grid row for driver X to driver number >> 1, NOP \ so drivers 0 and 1 are on row 0, drivers 2 and 3 are STA driverGridRow,X \ on row 1, and so on, up to row 9 at the back of the \ grid JSR SetDriverSpeed \ Set the base speed for driver X \ \ It also decrements X to the next driver number and \ updates setSpeedForDriver accordingly LDA #0 \ Zero (totalPointsTop totalPointsHi totalPointsLo) for STA totalPointsLo,X \ driver X STA totalPointsHi,X STA totalPointsTop,X TXA \ If X <> 0, loop back to driv1 to process the next BNE driv1 \ driver, until we have processed all 20 of them RTS \ Return from the subroutine
Name: PrintSecondLine [Show more] Type: Subroutine Category: Text Summary: Prints a text token on the second text line at the top of the driving screen
Context: See this subroutine on its own page References: This subroutine is called as follows: * PrintSecondLineGap calls PrintSecondLine * ResetVariables calls PrintSecondLine

Arguments: X The token number (0 to 54)
.PrintSecondLine LDA #33 \ Set A = 33 to use as the value for yCursor below, so \ we print the text token on the second text line at the \ top of the driving screen BNE PrintFirstLine+2 \ Jump to PrintFirstLine+2 to print the token in X on \ the second text line in the driving screen (this BNE \ is effectively a JMP as A is never zero)
Name: PrintFirstLine [Show more] Type: Subroutine Category: Text Summary: Prints a text token on the first text line at the top of the driving screen
Context: See this subroutine on its own page References: This subroutine is called as follows: * PrintSecondLineGap calls PrintFirstLine * ResetVariables calls PrintFirstLine * UpdateLapTimers calls PrintFirstLine * PrintSecondLine calls entry point PrintFirstLine+2

Arguments: X The token number (0 to 54) Other entry points: PrintFirstLine+2 Print the token on the second text line at the top of the driving screen
.PrintFirstLine LDA #24 \ Set A = 24 to use as the value for yCursor below, so \ we print the text token on the first line of the two \ text lines at the top of the driving screen STA yCursor \ Move the cursor to pixel row A (which will either be \ the first or the second text line at the top of the \ screen) LDA #1 \ Move the cursor to character column 1 STA xCursor \ Fall through into PrintToken to print the token in X \ at (xCursor, yCursor)
Name: PrintToken [Show more] Type: Subroutine Category: Text Summary: Print a recursive token Deep dive: Text tokens
Addresses of token strings are in the (tokenHi tokenLo) table. Tokens are numbered from 0 to 54. Each token's string contains bytes that are printed as follows: * 0-159 Print character n * 160-199 Print n - 160 spaces (0 to 39) * 200-254 Print token n - 200 (0 to 54) * 255 End of token Arguments: X The token number (0 to 54) (xCursor, yCursor) The on-screen position for the token
.PrintToken LDY #0 \ We are about to work our way through the token, one \ byte at a time, so set a byte counter in Y .toke1 LDA tokenHi,X \ Set (S R) = the X-th entry in (tokenHi tokenLo), which STA S \ points to the string of bytes in token X LDA tokenLo,X STA R .toke2 LDA (R),Y \ Set A to the Y-th byte at (S R), which contains the \ next character in the token CMP #255 \ If A = 255 then we have reached the end of the token, BEQ toke8 \ so jump to toke8 to return from the subroutine CMP #200 \ If A < 200 then this byte is not another token, so BCC toke5 \ jump to toke5 SEC \ A >= 200, so this is a pointer to another token SBC #200 \ embedded in the current token, so subtract 200 to get \ the embedded token's number STA T \ Store the embedded token's number in T TXA \ Store X and Y on the stack so we can retrieve them PHA \ after printing the embedded token TYA PHA LDX T \ Set X to the number of the embedded token we need to \ print CPX #54 \ If X <> 54, jump to toke3 to skip the following three BNE toke3 \ instructions and print the embedded token LDX #0 \ X = 54, so call PrintHeader with X = 0 to print JSR PrintHeader \ "FORMULA 3 CHAMPIONSHIP" as a double-height header \ at column 4, row 3, in yellow text on a red background JMP toke4 \ Skip the following instruction .toke3 JSR PrintToken \ Print token X (so if it also contains embedded tokens, \ they will also be expanded and printed) .toke4 PLA \ Retrieve X and Y from the stack TAY PLA TAX INY \ Increment the byte counter JMP toke1 \ Loop back to print the next byte in the token, making \ sure to recalculate (S R) as it will have been \ corrupted by the call to PrintToken .toke5 \ If we get here then A < 200, so this byte is not \ another token CMP #160 \ If A < 160, jump to toke6 to skip the following three BCC toke6 \ instructions SBC #160 \ A is in the range 160 to 199, so subtract 160 to get \ the number of spaces to print, in the range 0 to 39 JSR PrintSpaces \ Print the number of spaces in A BEQ toke7 \ Skip the following instruction (this BNE is \ effectively a JMP as PrintSpaces sets the Z flag) .toke6 JSR PrintCharacter \ Print the character in A (which is in the range 0 to \ 159) .toke7 INY \ Increment the byte counter JMP toke2 \ Loop back to print the next byte in the token .toke8 RTS \ Return from the subroutine
Name: ApplyVergeJump [Show more] Type: Subroutine Category: Driving model Summary: Apply a jump to the player's car when hitting the track verge
Context: See this subroutine on its own page References: This subroutine is called as follows: * ApplyGrassOrTrack calls ApplyVergeJump

Calculate the following: yGravityDelta = playerSpeedHi / 2 yJumpHeight = playerSpeedHi / 4 heightAboveTrack = heightAboveTrack + 1 spinYawAngleHi = spinYawAngleHi >> 1 with bit 7 set and make the crash/contact sound.
.ApplyVergeJump LDA playerSpeedHi \ Set A = playerSpeedHi \ Fall through into ApplyBounce to calculate the \ variables and make the crash/contact sound
Name: ApplyBounce [Show more] Type: Subroutine Category: Driving model Summary: Apply a bounce to the player's car when it hits the ground
Context: See this subroutine on its own page References: This subroutine is called as follows: * ApplyElevation (Part 4 of 5) calls ApplyBounce

Calculate the following: yGravityDelta = A / 2 yJumpHeight = A / 4 heightAboveTrack = heightAboveTrack + 1 spinYawAngleHi = spinYawAngleHi >> 1 with bit 7 set and make the crash/contact sound.
.ApplyBounce LSR A \ Set yGravityDelta = A / 2 STA yGravityDelta LSR A \ Set yJumpHeight = A / 4 STA yJumpHeight INC heightAboveTrack \ Set heightAboveTrack = heightAboveTrack + 1 SEC \ Rotate spinYawAngleHi right, inserting a 1 into bit 7 ROR spinYawAngleHi LDA #4 \ Make sound #4 (crash/contact) at the current volume JSR MakeSound-3 \ level RTS \ Return from the subroutine
Name: SetCustomScreen [Show more] Type: Subroutine Category: Screen mode Summary: Switch to the custom screen mode Deep dive: Hidden secrets of the custom screen mode
Context: See this subroutine on its own page References: This subroutine is called as follows: * MainDrivingLoop (Part 1 of 5) calls SetCustomScreen

Returns: screenSection screenSection is set to -2, so the interrupt handler at ScreenHandler does not do anything straight away, but leaves the palette mapped to black, so the screen is blank
.SetCustomScreen SEI \ Disable interrupts so we can update the 6845 registers \ First we switch screen mode to the custom screen mode \ used for the race, which is based on mode 5 but is \ shorter at 26 character rows rather than 40 \ \ We do this by first reprogramming registers R0 to R13 \ of the 6845 CRTC chip using the values in the \ screenRegisters table (see the screenRegisters \ variable for details), and then programming register 0 \ of the Video ULA to the same value as standard mode 5 LDX #13 \ We are about to write values into registers R0 to R13 \ so set a register counter in X to count down from 13 \ to 0 .cust1 STX VIA+&00 \ Put register number X into SHEILA &00, so we can now \ set the value of this 6845 register LDA screenRegisters,X \ Set register X to the X-th value of screenRegisters STA VIA+&01 DEX \ Decrement the register counter BPL cust1 \ Loop back until we have set registers R0 to R13 to the \ values in the screenRegisters table DEX \ Set screenSection = -2, as the above loop finishes STX screenSection \ with X = -1 CLI \ Re-enable interrupts LDA #154 \ Call OSBYTE with A = 154 to set register 0 of the LDX #%11000100 \ Video ULA to the value in X, which sets the following, JSR OSBYTE \ reading from bit 7 to bit 0: \ \ %1 = master cursor size = large cursor \ %10 = width of cursor in bytes = 2 \ %0 = 6845 clock rate select = low frequency clock \ %01 = number of characters per line = 20 \ %0 = teletext output select = on-chip serialiser \ %0 = flash colour select = first colour selected \ \ These values are the same as in standard mode 5, and \ this call finishes the switch to our custom screen \ mode CLC \ Clear the C flag for the additions in the following \ loop \ We now send the following bytes to the Video ULA \ palette in SHEILA &21, by starting at 7 and adding &10 \ to send &07, &17, &27 ... &E7, &F7 \ \ This maps all four logical colours (the top nibble) to \ &7 EOR 7 (the bottom nibble, EOR 7), which maps them \ to colour 0, or black LDA #&07 \ Set A = &07 as the first byte to send .cust2 STA VIA+&21 \ Send A to SHEILA &21 to send the palette byte in A to \ the Video ULA ADC #&10 \ Set A = A + &10 BCC cust2 \ Loop back until the addition overflows after we send \ &F7 to the ULA SEI \ Disable interrupts so we can update the VIAs LDA IRQ1V \ Store the current address from the IRQ1V vector in STA irq1Address \ irq1Address, so the IRQ handler can jump to it after LDA IRQ1V+1 \ implementing the custom screen mode STA irq1Address+1 LDA #2 \ This instruction appears to have no effect, as we are \ about to overwrite A and the processor flags .cust3 BIT VIA+&4D \ Read the 6522 System VIA interrupt flag register IFR \ (SHEILA &4D), which has bit 1 set if vertical sync \ has occurred on the video system BEQ cust3 \ Loop back to cust3 to keep reading the System VIA \ until the vertical sync occurs LDA #%01000000 \ Set 6522 User VIA auxiliary control register ACR STA VIA+&6B \ (SHEILA &6B) bits 7 and 6 to disable PB7 (which is one \ of the pins on the user port) and set continuous \ interrupts for timer 1 ORA VIA+&4B \ Set 6522 System VIA auxiliary control register ACR STA VIA+&4B \ (SHEILA &6B) bit 6 to set continuous interrupts for \ timer 1 LDA #%11000000 \ Set 6522 User VIA interrupt enable register IER STA VIA+&6E \ (SHEILA &4E) bits 6 and 7 (i.e. enable the Timer1 \ interrupt from the User VIA) STA VIA+&4E \ Set 6522 System VIA interrupt enable register IER \ (SHEILA &4E) bits 6 and 7 (i.e. enable the Timer1 \ interrupt from the System VIA) LDA #&D4 \ Set 6522 User VIA T1C-L timer 1 low-order counter to STA VIA+&64 \ (SHEILA &64) to &D4 (so this sets the low-order \ counter but does not start counting until the \ high-order counter is set) LDA #&11 \ Set 6522 User VIA T1C-H timer 1 high-order counter STA VIA+&65 \ (SHEILA &45) to &11 to start the T1 counter \ counting down from &1164 (4452) at a rate of 1 MHz LDA #&01 \ Set 6522 System VIA T1L-L timer 1 low-order latches STA VIA+&46 \ to &01 (so this sets the low-order counter but does \ not start counting until the high-order counter is \ set) LDA #&3D \ Set 6522 System VIA T1C-H timer 1 high-order counter STA VIA+&45 \ to &3D, to start the T1 counter counting down from \ &3D01 LDA #&1E \ Set 6522 System VIA T1L-L timer 1 low-order latches STA VIA+&46 \ to &1E (so this sets the low-order counter but does \ not start counting until the high-order counter is \ set) STA VIA+&66 \ Set 6522 User VIA T1L-L timer 1 low-order latches \ to &1E (so this sets the low-order counter but does \ not start counting until the high-order counter is \ set) LDA #&4E \ Set 6522 System VIA T1L-H timer 1 high-order latches STA VIA+&47 \ to &4E (so this sets the timer to &4E1E (19998) but \ does not start counting until the current timer has \ run down) STA VIA+&67 \ Set 6522 User VIA T1L-H timer 1 high-order latches \ to &4E (so this sets the timer to &4E1E (19998) but \ does not start counting until the current timer has \ run down) LDA #HI(ScreenHandler) \ Set the IRQ1V vector to ScreenHandler, so the STA IRQ1V+1 \ ScreenHandler routine is now the interrupt handler LDA #LO(ScreenHandler) STA IRQ1V CLI \ Re-enable interrupts RTS \ Return from the subroutine
Name: ScreenHandler [Show more] Type: Subroutine Category: Screen mode Summary: The IRQ handler for the custom screen mode Deep dive: Hidden secrets of the custom screen mode
Context: See this subroutine on its own page References: This subroutine is called as follows: * SetCustomScreen calls ScreenHandler

The screen handler starts a new screen with screenSection = -1, and then increments it through 0, 1, 2, 3, 4 and 5, at which point this handler stops doing anything. Other entry points: ScreenHandler-3 Jump to the original IRQ handler
JMP (irq1Address) \ Jump to the original address from IRQ1V to pass \ control to the next interrupt handler .ScreenHandler LDA VIA+&6D \ Set A to the 6522 User VIA interrupt flag register IFR \ (SHEILA &46D) AND #%01000000 \ Extract bit 6, which is set when 6522 User VIA timer 1 \ runs down to zero BEQ ScreenHandler-3 \ If the Timer1 interrupt has not fired, jump up to \ ScreenHandler-3 as we do not need to do anything at \ this point STA VIA+&6D \ Set bit 6 of the 6522 User VIA interrupt flag register \ IFR (SHEILA &6D) to clear the timer 1 interrupt (the \ timer will already have restarted as we set it to \ continuous interrupts in SetCustomScreen) TXA \ Store X on the stack so we can preserve it through the PHA \ interrupt handler CLD \ Clear the D flag to switch arithmetic to normal LDA screenSection \ If screenSection = 0, jump to hand1 BEQ hand1 BMI hand4 \ If screenSection is negative, jump to hand4 CMP #2 \ If screenSection < 2, i.e. screenSection = 1, jump to BCC hand5 \ hand5 BEQ hand7 \ If screenSection = 2, jump to hand7 CMP #3 \ If screenSection = 3, jump to hand9 BEQ hand9 BCS hand11 \ If screenSection >= 3, i.e. screenSection = 4, jump \ to hand11 .hand1 \ If we get here, then screenSection = 0, so we set the \ screen mode to 4 and the palette for the top two lines \ of text (where the race information is printed) LDA #%10001000 \ Set the Video ULA control register (SHEILA &20) to STA VIA+&20 \ %10001000, which is the same as switching to mode 4 LDX #15 \ We now send the 16 palette bytes at paletteSection0 to \ the Video ULA palette in SHEILA &21, so set a loop \ counter in X .hand2 LDA paletteSection0,X \ Set the X-th byte of paletteSection0 to the Video ULA STA VIA+&21 \ palette DEX \ Decrement the loop counter BPL hand2 \ Loop back until we have sent all 16 bytes .hand3 LDA #&C4 \ Set (X A) = &0FC4 to latch into the User VIA timer 1, LDX #&0F \ so on the next timer loop it counts down from &0FC4 \ (4036) BNE hand13 \ Jump to hand13 to latch (X A) into User VIA timer 1 \ and return from the subroutine (this BNE is \ effectively a JMP as X is never zero) .hand4 \ If we get here, then screenSection is negative CMP #&FF \ If screenSection <> -1, then jump to hand14 to BNE hand14 \ return from the interrupt handler \ If we get here, then screenSection = -1 INC screenSection \ Set screenSection = 0 BEQ hand3 \ Jump to hand3 to set timer 1 counting down from &0FC4 \ and return from the interrupt handler (this BNE is \ effectively a JMP as screenSection is always zero) .hand5 \ If we get here, then screenSection = 1, so we change \ the palette so everything is blue, as this is the \ portion of cloudless sky between the text at the top \ of the screen and the car and track at the bottom LDA #%11000100 \ Set the Video ULA control register (SHEILA &20) to STA VIA+&20 \ %11000100, which is the same as switching to mode 5 CLC \ Clear the C flag for the additions in the following \ loop \ We now send the following bytes to the Video ULA \ palette in SHEILA &21, by starting at 3 and adding &10 \ to send &03, &13, &23 ... &E3, &F3 \ \ This maps all four logical colours (the top nibble) to \ &3 EOR 7 (the bottom nibble, EOR 7), which maps them \ to colour 4, or blue LDA #&03 \ Set A = &03 as the first byte to send .hand6 STA VIA+&21 \ Send A to SHEILA &21 to send the palette byte in A to \ the Video ULA ADC #&10 \ Set A = A + &10 BCC hand6 \ Loop back until the addition overflows after we send \ &F3 to the ULA LDA #&3C \ Set screenTimer2 = &153C - screenTimer1 SEC \ SBC screenTimer1 \ starting with the low bytes STA screenTimer2 LDA #&15 \ And then the high bytes SBC screenTimer1+1 STA screenTimer2+1 LDA screenTimer1 \ Set (X A) = screenTimer1 to latch into the User VIA LDX screenTimer1+1 \ timer 1, so on the next timer loop it counts down from \ screenTimer1 BCS hand13 \ Jump to hand13 to latch (X A) into User VIA timer 1 \ and return from the subroutine (this BCS is \ effectively a JMP as the C flag is still set from \ above) .hand7 \ If we get here, then screenSection = 2 LDX #15 \ We now send the 16 palette bytes at paletteSection2 to \ the Video ULA palette in SHEILA &21, so set a loop \ counter in X .hand8 LDA paletteSection2,X \ Set the X-th byte of paletteSection2 to the Video ULA STA VIA+&21 \ palette DEX \ Decrement the loop counter BPL hand8 \ Loop back until we have sent all 16 bytes LDA screenTimer2 \ Set (X A) = screenTimer2 to latch into the User VIA LDX screenTimer2+1 \ timer 1, so on the next timer loop it counts down from \ screenTimer2 BNE hand13 \ Jump to hand13 to latch (X A) into User VIA timer 1 \ and return from the subroutine (this BNE is \ effectively a JMP as X is never zero) .hand9 \ If we get here, then screenSection = 3 LDX #3 \ We now send the 3 palette bytes at paletteSection3 to \ the Video ULA palette in SHEILA &21, so set a loop \ counter in X .hand10 LDA paletteSection3,X \ Set the X-th byte of paletteSection2 to the Video ULA STA VIA+&21 \ palette DEX \ Decrement the loop counter BPL hand10 \ Loop back until we have sent all 16 bytes LDA #&00 \ Set (X A) = &1E00 to latch into the User VIA timer 1, LDX #&1E \ so on the next timer loop it counts down from &1E00 \ (7680) BNE hand13 \ Jump to hand13 to latch (X A) into User VIA timer 1 \ and return from the subroutine (this BNE is \ effectively a JMP as X is never zero) .hand11 \ If we get here, then screenSection = 4 LDX #3 \ We now send the 3 palette bytes at paletteSection4 to \ the Video ULA palette in SHEILA &21, so set a loop \ counter in X .hand12 LDA paletteSection4,X \ Set the X-th byte of paletteSection2 to the Video ULA STA VIA+&21 \ palette DEX \ Decrement the loop counter BPL hand12 \ Loop back until we have sent all 16 bytes STX screenSection \ Set screenSection = -1, as the above loop finishes \ with X = 255 JSR AnimateTyres \ Animate the tyres on either side of the screen LDA #&FF \ Set 6522 User VIA T2C-H timer 2 high-order counter STA VIA+&69 \ (SHEILA &69) to &FF to start the T2 counter \ counting down from &FFxx at a rate of 1 MHz LDA #&16 \ Set (X A) = &0B16 to latch into the User VIA timer 1, LDX #&0B \ so on the next timer loop it counts down from &0B16 \ (2838) .hand13 STX VIA+&67 \ Set 6522 User VIA T1L-H and T1L-L to set both timer 1 STA VIA+&66 \ latches (so this sets the timer to (X A) but does not \ start counting until the current timer has run down) INC screenSection \ Increment the screen section counter to move on to the \ next section .hand14 PLA \ Restore X from the stack TAX LDA &FC \ Set A to the interrupt accumulator save register, \ which restores A to the value it had on entering the \ interrupt RTI \ Return from interrupts, so this interrupt is not \ passed on to the next interrupt handler, but instead \ the interrupt terminates here
Name: screenRegisters [Show more] Type: Variable Category: Screen mode Summary: The 6845 registers for the custom screen mode Deep dive: Hidden secrets of the custom screen mode
Context: See this variable on its own page References: This variable is used as follows: * SetCustomScreen calls screenRegisters

The custom screen mode used during the race is based on standard mode 5, but with the following differences: * Horizontal sync position = 45 instead of 49 * Vertical displayed = 26 instead of 32 * Vertical sync position = 32 instead of 34 * Screen memory start = &5A80 instead of &5800 So essentially it is a shorter mode 5 that takes up less memory, adjusts the vertical and horizontal sync positions accordingly, and lives in screen memory from &5A80 to &7AFF (as there are 26 character rows of 40 characters, with 8 bytes per character, giving 26 * 40 * 8 = 8320 bytes of screen memory, and &5A80 + 8320 = &7B00).
.screenRegisters EQUB 63 \ Set 6845 register R0 = 63 \ \ This is the "horizontal total" register, which sets \ the horizontal sync frequency, i.e. the number of \ horizontal characters minus one. This value is the \ same as in standard mode 5 EQUB 40 \ Set 6845 register R1 = 40 \ \ This is the "horizontal displayed" register, which \ defines the number of character blocks per horizontal \ character row. This value is the same as in standard \ mode 5 EQUB 49 \ Set 6845 register R2 = 45 \ \ This is the "horizontal sync position" register, which \ defines the position of the horizontal sync pulse on \ the horizontal line in terms of character widths from \ the left-hand side of the screen. For comparison this \ is 49 for mode 5, but is adjusted for our custom \ screen EQUB &24 \ Set 6845 register R3 = &24 \ \ This is the "sync width" register, which sets the \ horizontal sync width in characters using the low \ nibble (i.e. 4), and the vertical sync width in the \ high nibble (i.e. 2). These values are the same as in \ standard mode 5 EQUB 38 \ Set 6845 register R4 = 38 \ \ This is the "vertical total" register, which contains \ the integer part of the vertical sync frequency minus \ one. This value is the same as in standard mode 5 EQUB 0 \ Set 6845 register R5 = 0 \ \ This is the "vertical total adjust" register, which \ contains the fractional part of the vertical sync \ frequency. This value is the same as in standard mode \ 5 EQUB 26 \ Set 6845 register R6 = 26 \ \ This is the "vertical displayed" register, which sets \ the number of displayed character rows to 26. For \ comparison, this value is 32 for standard modes 4 and \ 5, but we claw back six rows for storing code above \ the end of screen memory EQUB 32 \ Set 6845 register R7 = 32 \ \ This is the "vertical sync position" register, which \ determines the vertical sync position with respect to \ the reference, programmed in character row times. For \ comparison this is 34 for mode 5, but needs to be \ adjusted for our custom screen's vertical sync EQUB %00000001 \ Set 6845 register R8 = %00000001 \ \ This is the "interlace and display" register, which \ sets the following, reading from bit 7 to bit 0: \ \ %00 = no delay in the cursor blanking signal \ %00 = no delay in the display blanking signal \ %00 = not used \ %01 = interlace sync mode \ \ These values are the same as in standard mode 5 EQUB 7 \ Set 6845 register R9 = 7 \ \ This is the "scan lines per character" register, and \ contains the number of scan lines per character row, \ including spacing, minus one. This value is the same \ as in standard mode 5 EQUB %01100111 \ Set 6845 register R10 = %01100111 \ \ This is the "cursor start" register, which sets the \ following, reading from bit 7 to bit 0: \ \ %0 = not used \ %1 = enable blink feature \ %1 = set blink frequency to 32 times the field rate \ %00111 = cursor end scan line \ \ These values are the same as in standard mode 5 EQUB 8 \ Set 6845 register R11 = 8 \ \ This is the "cursor end" register, which sets the \ cursor end scan line. This value is the same as in \ standard mode 5 EQUB &0B \ Set 6845 register R12 = &0B and R13 = &50 EQUB &50 \ \ This sets 6845 registers (R12 R13) = &0B50 to point \ to the start of screen memory in terms of character \ rows. There are 8 pixel lines in each character row, \ so to get the actual address of the start of screen \ memory, we multiply by 8: \ \ &0B50 * 8 = &5A80 \ \ So this sets the start of screen memory to &5A80
Name: irq1Address [Show more] Type: Variable Category: Screen mode Summary: Stores the previous value of IRQ1V before we install our custom IRQ handler
Context: See this variable on its own page References: This variable is used as follows: * KillCustomScreen calls irq1Address * ScreenHandler calls irq1Address * SetCustomScreen calls irq1Address
.irq1Address EQUW 0
Name: screenTimer1 [Show more] Type: Variable Category: Screen mode Summary: The screen timer offset between the start of section 2 and the start of section 3 Deep dive: Hidden secrets of the custom screen mode
Context: See this variable on its own page References: This variable is used as follows: * MoveHorizon calls screenTimer1 * ScreenHandler calls screenTimer1
.screenTimer1 EQUW &04D8
Name: screenTimer2 [Show more] Type: Variable Category: Screen mode Summary: The screen timer offset between the start of section 3 and the start of section 4 Deep dive: Hidden secrets of the custom screen mode
Context: See this variable on its own page References: This variable is used as follows: * ScreenHandler calls screenTimer2
.screenTimer2 EQUW &1064
Name: KillCustomScreen [Show more] Type: Subroutine Category: Screen mode Summary: Disable the custom screen mode and switch to mode 7 Deep dive: Hidden secrets of the custom screen mode
Context: See this subroutine on its own page References: This subroutine is called as follows: * MainDrivingLoop (Part 5 of 5) calls KillCustomScreen
.KillCustomScreen SEI \ Disable interrupts so we can update the interrupt \ vector and VIA LDA irq1Address \ Set the IRQ1V vector to irq1Address, which removes the STA IRQ1V \ custom screen interrupt handler from the chain LDA irq1Address+1 STA IRQ1V+1 LDA #%01000000 \ Set 6522 User VIA interrupt enable register IER STA VIA+&6E \ (SHEILA &4E) bit 6 (i.e. disable the Timer1 interrupt \ from the User VIA, as we no longer neeed it) CLI \ Re-enable interrupts JSR FlushSoundBuffers \ Flush all four sound channel buffers \ Fall througn into SetScreenMode7 to switch to mode 7
Name: SetScreenMode7 [Show more] Type: Subroutine Category: Screen mode Summary: Change to screen mode 7 and hide the cursor
Context: See this subroutine on its own page References: This subroutine is called as follows: * SetupGame calls SetScreenMode7
.SetScreenMode7 LDA #128 \ Set printMode = 128 so the call to PrintToken prints STA printMode \ characters using OSWRCH (for mode 7) LDX #46 \ Print token 46, which changes to screen mode 7 and JSR PrintToken \ hides the cursor RTS \ Return from the subroutine
Name: screenSection [Show more] Type: Variable Category: Screen mode Summary: The section of the screen that is currently being drawn by the custom screen interrupt handler (0 to 4) Deep dive: Hidden secrets of the custom screen mode
Context: See this variable on its own page References: This variable is used as follows: * MainDrivingLoop (Part 3 of 5) calls screenSection * ScreenHandler calls screenSection * SetCustomScreen calls screenSection
.screenSection EQUB 0
Name: MoveHorizon [Show more] Type: Subroutine Category: Screen mode Summary: Move the position of the horizon palette switch up or down, depending on the current track pitch angle Deep dive: Hidden secrets of the custom screen mode
Context: See this subroutine on its own page References: This subroutine is called as follows: * MainDrivingLoop (Part 2 of 5) calls MoveHorizon
.MoveHorizon LDA #60 \ Set A = 60 - horizonLine SEC \ SBC horizonLine \ So A is larger when the horizon is low (i.e. when we \ are cresting a hill), and smaller when the horizon is \ high (i.e. when we are in a dip) BPL hori1 \ If A >= 0, then horizonLine <= 60, so jump to hori1 CMP #&F5 \ If A >= -11, then horizonLine <= 71, so jump to hori2 BCS hori2 \ with the C flag set LDA #&F5 \ Otherwise set A = -11 and set the C flag, so A has a SEC \ minimum value of -11 BCS hori2 \ Jump to hori2 (this BCS is effectively a JMP as we \ just set the C flag) .hori1 \ If we get here then A >= 0, i.e. horizonLine <= 60 CMP #18 \ If A < 18, jump to hori2 to skip the following two BCC hori2 \ instructions LDA #18 \ Otherwise set A = 18 and clear the C flag, so A has a CLC \ maximum value of 18 .hori2 PHP \ Store the C flag on the stack, which will be clear if \ A >= 0, or set if A < 0 (so the C flag is effectively \ the sign bit of A) STA U \ Set (U A) = (A 0) LDA #0 \ = A * 256 \ \ where -11 <= A < 18 and the sign bit of A is in C ROR U \ Set (U A) = (U A) >> 1, inserting the sign bit from C ROR A \ into bit 7 PLP \ Set the C flag to the sign bit once again ROR U \ Set (U A) = (U A) >> 1, inserting the sign bit from C ROR A \ into bit 7 \ \ So by this point, we have: \ \ (U A) = A * 256 / 4 \ = A * 64 \ \ with the correct sign, so (U A) is in the range -704 \ to 1152, and is larger when the horizon is low (i.e. \ when we are cresting a hill), and smaller when the \ horizon is high (i.e. when we are in a dip) \ \ We now add this figure to screenTimer1, which \ determines the height of the horizon portion of the \ custom screen mode, i.e. where the palette switches \ from blue sky to the green ground \ \ So when we are cresting a hill, (U A) is large and so \ is timer 1, and therefore so is the size of the sky \ above the horizon in section 2 of the screen, so the \ horizon dips down \ \ Conversely, when we are in a dip, (U A) is small and \ and so is timer 1, so the size of the sky section \ above the horizon is smaller, so the horizon rises up \ \ The range of screenTimer1 values from the following \ calculation is therefore: \ \ Minimum: &04D8 - 704 = &0218 (we are in a dip) \ \ Maximum: &04D8 + 1152 = &0958 (we are on a hill) SEI \ Disable interrupts so we can update the custom screen \ variables CLC \ Set screenTimer1 = (U A) + &04D8 ADC #&D8 \ STA screenTimer1 \ starting with the low bytes LDA #&04 \ And then the high bytes ADC U STA screenTimer1+1 CLI \ Re-enable interrupts RTS \ Return from the subroutine
Name: UpdateLaps [Show more] Type: Subroutine Category: Drivers Summary: Increment the lap number and lap times for a specific driver
Context: See this subroutine on its own page References: This subroutine is called as follows: * MoveObjectForward calls UpdateLaps

Update the lap number and lap times for driver X, but only if the following are true: * Driver number is in the range 0 to 19 * Bit 7 of updateLapTimes is clear * Bit 6 of the driver's car's object status byte is clear (so the car is still racing) * If this is the current player, pastHalfway must be 1 (so the player is in the second half of the track) If these conditions are met, then the lap is incremented. Arguments: X Driver number (only drivers 0 to 19 have lap times) updateLapTimes If bit 7 is set, this routine does nothing
.UpdateLaps BIT updateLapTimes \ If bit 7 of updateLapTimes is set, jump to ulap1 to BMI ulap1 \ return from the subroutine without doing anything CPX #20 \ If X >= 20 then this is not a driver number, so jump BCS ulap1 \ to ulap1 to return from the subroutine LDA objectStatus,X \ If bit 6 of the driver's car object status byte is ASL A \ set, then the car has finished racing, so jump to BMI ulap1 \ ulap1 to return from the subroutine without updating \ the lap number CPX currentPlayer \ If driver X is not the current player, jump to ulap3 BNE ulap3 \ to skip updating the lap number top of the screen \ If we get here then this is the current player, so now \ we check the value of pastHalfway DEC pastHalfway \ If pastHalfway = 1, then the player is in the second BEQ ulap2 \ half of the track, so set pastHalfway to 0 as the INC pastHalfway \ player has just passwd the starting line into the \ first half of the track, and jump to ulap2 to update \ the lap details .ulap1 RTS \ Return from the subroutine .ulap2 \ If we get here then this is the current player, and \ pastHalfway has just been changed from 1 to 0, to \ denote a new lap LDA #%10000000 \ Set bit 7 and clear bit 6 of updateDrivingInfo so the STA updateDrivingInfo \ lap number gets updated at the top of the screen .ulap3 \ If we get here then we have passed all the checks, so \ it's time to increment the lap number and times, \ starting with the lap number LDA driverLapNumber,X \ Set A to the current lap number for driver X BMI ulap4 \ If A is negative, skip the following instruction INC driverLapNumber,X \ Increment the current lap number for driver X .ulap4 \ We now decide whether to increment the lap times BIT raceStarted \ If bit 7 of raceStarted is clear then this is practice BPL ulap5 \ or qualifying, so jump to ulap5 \ If we get here then this is a race CMP numberOfLaps \ If the current lap number for driver X < the number BCC ulap7 \ of laps in the race, then this is not the last lap in \ the race, so jump to ulap7 BEQ ulap6 \ If the current lap number for driver X = the number \ of laps in the race, then this is the last lap in \ the race, so jump to ulap6 \ If we get here then the current lap number is bigger \ than the number of laps in the race, which means the \ driver has already finished the race and is still \ driving, so we don't increment the lap times RTS \ Return from the subroutine .ulap5 \ If we get here then this is practice or qualifying CPX currentPlayer \ If X <= the driver number of the current player, jump BEQ ulap7 \ to ulap7 BCC ulap7 \ If we get here then driver X has a bigger number than \ the current player, so we ignore it (is this because \ the only players with numbers higher than the current \ player are other players, rather than drivers) RTS \ Return from the subroutine .ulap6 \ If we get here then this is a race and this is the \ last lap in the race, so this driver just finished the \ race (as this routine is all about incrementing the \ lap number) CPX currentPlayer \ If X <> the driver number of the current player, jump BNE ulap7 \ to ulap7 \ If we get here then driver X is the current player, so \ the current player just finished the race LDA #80 \ Set leaveTrackTimer = 80, so we leave the track in 80 STA leaveTrackTimer \ main loop iterations and return to the game menu .ulap7 \ If we get here, then it's time to increment the lap \ times SED \ Set the D flag to switch arithmetic to Binary Coded \ Decimal (BCD) \ We now subtract driver X's total race time from the \ current clock time, to see whether this is a new best \ time SEC \ Set T = clockTenths - totalRaceTenths for driver X LDA clockTenths SBC totalRaceTenths,X STA T LDA clockSeconds \ Set A = clockSeconds - totalRaceSeconds for driver X SBC totalRaceSeconds,X BCS ulap8 \ If the subtraction underflowed, add 60 seconds to the ADC #&60 \ result CLC .ulap8 STA U \ Set U = A \ = clockSeconds - totalRaceSeconds for driver X LDA clockMinutes \ Set H = clockMinutes - totalRaceMinutes for driver X SBC totalRaceMinutes,X STA H \ So by this point, (H U T) contains the time difference \ between the clock time and driver X's total race time, \ which is the current lap time (as we last updated the \ driver's total time at the end of the last lap) BCC ulap9 \ If the subtraction underflowed, then somehow the clock \ timer is showing a smaller time than driver X's total \ race time, so this isn't a new best lap time, and we \ jump to ulap9 SEC \ Subtract (H U T) - driver X's best lap time LDA T SBC bestLapTenths,X LDA U SBC bestLapSeconds,X LDA H SBC bestLapMinutes,X BCS ulap9 \ If the subtraction didn't underflow, then (H U T) is \ bigger than driver X's best lap time, so this isn't a \ new best lap time, so jump to ulap9 \ (H U T) is lower than driver X's best lap time, so we \ have a new best lap time LDA T \ Set driver X's best lap time to (H U T), setting the AND #&F0 \ second digit of the tenths figure to 0 STA bestLapTenths,X LDA U STA bestLapSeconds,X LDA H STA bestLapMinutes,X .ulap9 LDA clockTenths \ Set the total race time for driver X to the clock time STA totalRaceTenths,X \ so we can use it to work out the lap time for the next LDA clockSeconds \ lap STA totalRaceSeconds,X LDA clockMinutes STA totalRaceMinutes,X CLD \ Clear the D flag to switch arithmetic to normal RTS \ Return from the subroutine NOP \ These instructions have no effect - presumably they NOP \ are left over from changes during development
Name: ZeroTimer [Show more] Type: Subroutine Category: Drivers Summary: Zero the specified timer
Context: See this subroutine on its own page References: This subroutine is called as follows: * AddTimeToTimer calls ZeroTimer * MainDrivingLoop (Part 1 of 5) calls ZeroTimer * ResetVariables calls ZeroTimer * UpdateLapTimers calls ZeroTimer

Arguments: X The timer to set to zero: * 0 = the clock timer (clockMinutes clockSeconds clockTenths) * 1 = the lap timer (lapMinutes lapSeconds lapTenths) Returns: A A = 0 and the Z flag is set (so a BEQ will branch)
.ZeroTimer LDA #0 \ Zero clockTenths or lapTenths STA clockTenths,X STA clockSeconds,X \ Zero clockSeconds or lapSeconds STA clockMinutes,X \ Zero clockMinutes or lapMinutes RTS \ Return from the subroutine
Name: PrintBestLapTime [Show more] Type: Subroutine Category: Text Summary: Print the best lap time and the current lap time at the top of the screen
Context: See this subroutine on its own page References: This subroutine is called as follows: * ResetVariables calls PrintBestLapTime * UpdateLapTimers calls PrintBestLapTime
.PrintBestLapTime LDX #32 \ Move the cursor to character column 32 (to just after STX xCursor \ "Best time" in token 40) INX \ Move the cursor to pixel row 33 (i.e. the second text STX yCursor \ line at the top of the screen) LDX currentPlayer \ Set X to the driver number of the current player, so \ the call to PrintTimer prints the lap time for the \ current driver LDA #%00100110 \ Print the best lap time for driver X in the following JSR PrintTimer \ 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 \ Fall through into PrintLapTime to print the current \ lap time at the top of the screen
Name: PrintLapTime [Show more] Type: Subroutine Category: Text Summary: Print the current lap time at the top of the screen
Context: See this subroutine on its own page References: This subroutine is called as follows: * UpdateLapTimers calls PrintLapTime * UpdateLapTimers calls entry point PrintLapTime+2

This routine prints the current lap time in the header 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 Other entry points: PrintLapTime+2 Format the lap time using the format value in A (see PrintTimer for details)
.PrintLapTime LDA #%00101000 \ Set A so the current lap time is printed in the \ following format by the call to PrintTimer: \ \ * %00 Minutes: No leading zeroes, print both digits \ * %10 Seconds: Leading zeroes, print both digits \ * %1 Tenths: Do not print tenths of a second LDX #10 \ Move the cursor to character column 10 (to just after STX xCursor \ "Lap time" in token 40) LDX #33 \ Move the cursor to pixel row 33 (i.e. the second text STX yCursor \ line at the top of the screen) LDX #21 \ Print (lapMinutes lapSeconds lapTenths) in the format JSR PrintTimer \ given in A RTS \ Return from the subroutine
Name: GetADCChannel [Show more] Type: Subroutine Category: Keyboard Summary: Read the value of an ADC channel (used to read the joystick)
Context: See this subroutine on its own page References: This subroutine is called as follows: * ProcessDrivingKeys (Part 1 of 6) calls GetADCChannel * ProcessDrivingKeys (Part 3 of 6) calls GetADCChannel

This routine reads a joystick axis and returns a value with 0 representing the stick being at the centre point, and -127 and +127 representing the left/right or up/down values. Arguments: X The ADC channel to read: * 1 = joystick X * 2 = joystick Y Returns: A The high byte of the channel value, converted to an absolute figure in the range 0 to 127 X The sign of the result: * 0 = positive, i.e. right or down * 1 = negative, i.e. left or up C flag Clear if A < 10
.GetADCChannel LDA #128 \ Call OSBYTE with A = 128 to fetch the 16-bit value JSR OSBYTE \ from ADC channel X, returning (Y X), i.e. the high \ byte in Y and the low byte in X TYA \ Copy Y to A, so A contains the high byte of the \ channel value \ The channel value in A will be in the range 0 to 255, \ with 128 representing the stick being in the centre, \ so now we need to flip this around into the range 0 to \ 127, with the sign given in X LDX #1 \ Set X = 1, to denote a negative result (left or down), \ which we will change below if this is a positive \ result CLC \ Set A = A + 128, so in terms of 8-bit numbers, this ADC #128 \ does the following: \ \ * 0-127 goes to 128-255 \ \ * 128-255 goes to 256-383, i.e. 0-127 \ \ So A is now in the range 128 to 255 for low readings \ from the ADC (right or down), or 0 to 127 for high \ readings (left or up) BPL adcc1 \ If A is in the range 0 to 127, skip the following two \ instructions as the result is already in the correct \ range, 0 to 127, and X is set to 1 for left or up EOR #&FF \ Flip the value of A, so the range 128 to 255 flips to \ the range 127 to 0 DEX \ Set X = 0 to denote a positive result, right or down .adcc1 CMP #10 \ Clear the C flag if A < 10 RTS \ Return from the subroutine
Name: ProcessTime [Show more] Type: Subroutine Category: Main loop Summary: Increment the timers and the main loop counter Deep dive: Scheduling tasks in the main loop
Context: See this subroutine on its own page References: This subroutine is called as follows: * FinishRace calls ProcessTime * MainDrivingLoop (Part 2 of 5) calls ProcessTime
.ProcessTime \ First, we update the timerAdjust counter, which \ iterates from trackTimerAdjust down to zero and back \ round again, but only if trackTimerAdjust <> 255 \ \ The timerAdjust counter is used to control the speed \ of the timers in the AddTimeToTimer routine LDX timerAdjust \ If timerAdjust <> 0, jump to tick1 to decrement the BNE tick1 \ counter in timerAdjust, as it hasn't wrapped round yet \ If we get here then timerAdjust = 0, so we need to \ wrap round to trackTimerAdjust again LDX trackTimerAdjust \ Set X = trackTimerAdjust + 1 INX \ \ We add the 1 so we can decrement it back to \ trackTimerAdjust below (assuming timer adjustments are \ enabled) BEQ tick2 \ If X = 0, then trackTimerAdjust must be 255, in which \ case timer adjustments are disabled, so jump to tick2 \ to leave timerAdjust alone .tick1 DEX \ Set timerAdjust = X - 1 STX timerAdjust \ \ So the clock adjustment counter decrements on each \ iteration round the main loop .tick2 LDA raceStarting \ If bit 7 of raceStarting is set, then the race is in BMI tick3 \ the process of starting but hasn't started yet, so \ jump to tick3 to leave the clock timer alone LDX #0 \ Increment the clock timer JSR AddTimeToTimer .tick3 INC mainLoopCounterLo \ Increment the main loop counter in (mainLoopCounterHi \ mainLoopCounterLo), starting with the low byte BNE tick4 \ And then the high byte, if the low byte overflowed INC mainLoopCounterHi .tick4 LDA clockSeconds \ If clockSeconds = 0, skip the following BEQ tick5 LDA mainLoopCounterLo \ If mainLoopCounterLo mod 32 <> 0, which will be true AND #31 \ for 31 out of 32 iterations round the main loop, jump BNE tick6 \ to tick6 to return from the subroutine .tick5 \ We only get here when mainLoopCounterLo mod 31 = 0, \ which is once every 32 iterations of the main driving \ loop JSR SetDriverSpeed \ Set the speed for the driver number specified in \ setSpeedForDriver, and increment setSpeedForDriver so \ the next time we get here (in 32 iterations of the \ main loop) we set the speed for the next driver .tick6 RTS \ Return from the subroutine
Name: GetPositionAhead [Show more] Type: Subroutine Category: Drivers Summary: Decrement X to the previous position number (from 19 to 0 and round again), which gives the position ahead of X
Context: See this subroutine on its own page References: This subroutine is called as follows: * MoveAndDrawCars calls GetPositionAhead * ProcessOvertaking (Part 1 of 3) calls GetPositionAhead * SetDriverSpeed calls GetPositionAhead * SetPlayerPositions calls GetPositionAhead
.GetPositionAhead DEX \ Decrement X BPL prev1 \ If X is >= 0, jump to prev1 to skip the following \ instruction LDX #19 \ Set X = 19, so repeated calls to this routine will \ decrement X down to 0, and then start again at 19 .prev1 RTS \ Return from the subroutine
Name: GetPositionBehind [Show more] Type: Subroutine Category: Drivers Summary: Increment X to the next position number (from 0 to 19 and round again), which gives the position behind X
Context: See this subroutine on its own page References: This subroutine is called as follows: * DrawCars calls GetPositionBehind * MoveAndDrawCars calls GetPositionBehind * ProcessOvertaking (Part 3 of 3) calls GetPositionBehind * SetPlayerPositions calls GetPositionBehind
.GetPositionBehind INX \ Increment X CPX #20 \ If X < 20, jump to getb1 to skip the following BCC getb1 \ instruction LDX #0 \ Set X = 0, so repeated calls to this routine will \ increment X up to 19, and then start again at 0 .getb1 RTS \ Return from the subroutine
Name: PrintCharacter [Show more] Type: Subroutine Category: Text Summary: Print a character on-screen
Context: See this subroutine on its own page References: This subroutine is called as follows: * Print2DigitBCD calls PrintCharacter * PrintDriverName calls PrintCharacter * PrintSpaces calls PrintCharacter * PrintTimer calls PrintCharacter * PrintToken calls PrintCharacter * PrintGearNumber calls entry point PrintCharacter-6

Arguments: A Character number (ASCII code, 0 to 159) printMode Bit 7 determines how the character is printed on-screen: * 0 = poke the character directly into screen memory (for the custom screen mode) * 1 = print the character with OSWRCH (for mode 7) (xCursor, yCursor) For the custom screen only, this is the coordinate where we should print the character, where xCursor is the character column and yCursor is the pixel row of the bottom of the character Returns: A A is unchanged Other entry points: PrintCharacter-6 Print double-width character (this is used to print the double-width number on the gear stick) The routine must be called twice to print double-width characters, which are drawn as follows: * Bit 7 of W is set = draw the right half * Bit 7 of W is clear = draw the left half W must be non-zero when the routine is called on this entry point, otherwise the routine will print characters at the normal width
STA characterDef \ Store the character number in characterDef JMP char1 \ Jump to char1 to skip the printMode check and use the \ current value of W .PrintCharacter BIT printMode \ If bit 7 of printMode is set, jump to char8 to print BMI char8 \ the character in A with OSWRCH STA characterDef \ Store the character number in characterDef LDA #0 \ Set W = 0 to indicate we should print a single-width STA W \ character .char1 TXA \ Store X and Y on the stack so we can retrieve them PHA \ after we have printed the character TYA PHA LDY #HI(characterDef) \ Call OSWORD with A = 10 and (Y X) = characterDef, LDX #LO(characterDef) \ which puts the character definition for the specified LDA #10 \ character into characterDef+1 to characterDef+8 JSR OSWORD LDA W \ If W = 0, jump to char5 to skip the following BEQ char5 \ If we get here, then W is non-zero, so we now update \ the character definition to contain just one half of \ the character in the left half of the character \ definition, as follows: \ \ * If bit 7 of W is set, put the right half of the \ character into left half of the character \ definition \ \ * If bit 7 of W is clear, put the left half of the \ character into left half of the character \ definition LDX #8 \ We are now going to work our way through each pixel \ row of the character definition, so set X as a loop \ counter for each byte in the character definition .char2 LDA characterDef,X \ Set A to the bitmap for the X-th row of the character \ definition BIT W \ If bit 7 of W is set, jump to char3 to skip the next BMI char3 \ two instructions AND #%11110000 \ Clear the four pixels in the right half of the pixel JMP char4 \ row .char3 ASL A \ Shift A to the left so the right half of the pixel ASL A \ row moves to the left half ASL A ASL A .char4 STA characterDef,X \ Store the updated pixel row byte in the X-th row of \ the character definition DEX \ Decrement the row counter BNE char2 \ Loop back until we have processed all eight rows in \ the character definition .char5 LDY yCursor \ Set (Q P) to the screen address of the character block LDA xCursor \ containing character column xCursor and pixel row JSR GetScreenAddress-2 \ yCursor, and set Y to the pixel row number within that \ block \ \ As yCursor is the pixel row of the bottom of where we \ should print the character, (Q P) now points to the \ address where the bottom pixel row of the character \ should go LDX #8 \ We are now going to work our way through each pixel \ row of the character definition, poking each row to \ screen memory, from the bottom row of the character \ to the top, so set a counter in X for eight rows .char6 LDA characterDef,X \ Store the X-th row of the character definition in the STA (P),Y \ Y-th byte of (Q P) DEY \ Decrement the pixel row number to point to the row \ above BPL char7 \ If Y is positive then we are still within the \ character block, so jump to char7 LDA P \ Otherwise we need to move to the bottom pixel row of SEC \ the character row above, so set: SBC #&40 \ STA P \ (Q P) = (Q P) - &140 \ \ starting with the low bytes LDA Q \ And then the high bytes, so (Q P) contains the screen SBC #1 \ address of the character block above (as each STA Q \ character row contains &140 bytes) LDY #7 \ Set Y = 7 to point to the bottom pixel row in the new \ character block .char7 DEX \ Decrement the character pixel row counter BNE char6 \ Loop back to poke the next row into screen memory \ until we have poked all eight rows INC xCursor \ Move the cursor to the right by one character, as we \ have just printed a full character PLA \ Retrieve X and Y from the stack TAY PLA TAX LDA characterDef \ Set A to the character number, so A is unchanged by \ the routine RTS \ Return from the subroutine .char8 JSR OSWRCH \ Print the character in A RTS \ Return from the subroutine
Name: GetScreenAddress [Show more] Type: Subroutine Category: Drawing pixels Summary: Return the screen address for a specified screen coordinate
Context: See this subroutine on its own page References: This subroutine is called as follows: * UpdateDashboard calls GetScreenAddress * PrintCharacter calls entry point GetScreenAddress-2

Arguments: A The screen x-coordinate in pixels (0 to 159) Y The screen y-coordinate in pixels Returns: (Q P) The address of the character block containing the screen coordinates Y The pixel row within the character block containing the screen coordinates Other entry points: GetScreenAddress-2 Treat the x-coordinate as a character column number rather than a pixel coordinate (0 to 39)
ASL A \ Set A = A << 2 ASL A \ = x-coord << 2 \ \ so in the following, (Q P) gets set to x-coord << 3, \ or x-coord * 8, which gives us the correct byte number \ for this coordinate on the character row, as each \ character block contains eight bytes .GetScreenAddress STA P \ Set (Q P) = A << 1 LDA #0 \ = x-coord << 1 ASL P \ = x-coord * 2 ROL A \ STA Q \ so (Q P) contains the correct byte number for this \ coordinate as an offset from the start address of the \ character row, as each character row contains 320 \ bytes, and the x-coordinate in A is in the range 0 to \ 160 (i.e. each character block is two pixels wide) TYA \ Set X = Y LSR A \ = y-coord >> 3 LSR A \ LSR A \ so X is the character row number for this coordinate TAX \ The X-th entry in the (yLookupHi yLookupLo) table \ contains the screen address of the start of character \ row X in the custom screen, so we now add this to \ (Q P) to get the screen address of the correct \ character block on this row LDA yLookupLo,X \ Set (Q P) = (Q P) + X-th yLookup entry CLC \ ADC P \ starting with the low bytes STA P LDA yLookupHi,X \ And then the high bytes ADC Q STA Q TYA \ Set Y = Y mod 8, to set it to the pixel row within the AND #7 \ character block for the coordinate TAY RTS \ Return from the subroutine
Name: EraseRevCounter [Show more] Type: Subroutine Category: Dashboard Summary: Erase a line by replacing each pixel in the line with its original contents
Context: See this subroutine on its own page References: This subroutine is called as follows: * UpdateDashboard calls EraseRevCounter
.EraseRevCounter LDX lineBufferSize \ Set X to the size of the line buffer BEQ erev2 \ If the line buffer is empty, jump to erev2 to return \ from the subroutine, as there is no line to erase DEX \ Decrement X so that it can work as a buffer counter \ working through buffer entries X down to 0 .erev1 LDA lineBufferAddrLo,X \ Set (Q P) to the screen address of the X-th pixel in STA P \ the line buffer LDA lineBufferAddrHi,X STA Q LDA lineBufferPixel,X \ Set A to the original screen contents of the X-th in \ the line buffer LDY #0 \ Restore the pixel to its original screen content, i.e. STA (P),Y \ the pixel that was there before we drew a line over \ the top of it DEX \ Decrement the buffer counter BPL erev1 \ Loop back until we have restored all the pixels in the \ line buffer STY lineBufferSize \ Set lineBufferSize = 0, to reset the line buffer .erev2 RTS \ Return from the subroutine
Name: UpdateDashboard [Show more] Type: Subroutine Category: Dashboard Summary: Update the rev counter and steering wheel lines on the dashboard
Context: See this subroutine on its own page References: This subroutine is called as follows: * MainDrivingLoop (Part 5 of 5) calls UpdateDashboard
.UpdateDashboard JSR EraseRevCounter \ Erase the dial hand on the rev counter and the line on \ the steering wheel JSR DrawRevCounter \ Redraw the dial hand on the rev counter \ We now draw the line on the steering wheel LDA steeringLo \ Set T = steeringLo STA T LSR A \ Set the C flag to bit 0 of steeringLo (the sign bit) \ which is set if we are steering left, or clear if we \ are steering right PHP \ Store the C flag on the stack LDA #2 \ Set A = 2, to use as the value of V to send to the \ DrawDashboardLine routine (shallow slope, right and \ down) BCS upda1 \ If bit 0 of steeringLo is set, skip the following \ instruction LDA #5 \ Bit 0 of steeringLo is clear, so set A = 5 to use \ as the value of V to send to the DrawDashboardLine \ routine (shallow slope, left and down) .upda1 STA V \ Set V = A, which we will pass to the DrawDashboardLine \ routine LDA steeringHi \ Set A = steeringHi, so (A T) = (steeringHi steeringLo) ASL T \ Set (A T) = (A T) << 1 ROL A \ \ setting the C flag to the top bit of (A T) BCS upda2 \ If the C flag is set, skip the following four \ instructions to set A = 60, as the wheel is turned so \ much that the indicator would be off the bottom of the \ screen CMP #38 \ If A < 38, jump to upda4 BCC upda4 CMP #61 \ If A < 61, i.e. 38 <= A <= 60, jump to upda3 to BCC upda3 \ skip the following instruction .upda2 LDA #60 \ Set A = 60, the maximum value of A, so when we fall \ through into the next calculation, with the C flag \ set, we set: \ \ Y = ~A + 76 + C \ = ~A + 76 + 1 \ = ~A + 1 + 76 \ = -A + 76 \ = -60 + 76 \ = 16 .upda3 \ If we get here then the indicator is a long way away \ from the centre of the wheel, as A >= 38 EOR #&FF \ Set Y = ~A + 76 + C ADC #76 \ = ~A + 1 + 75 + C TAY \ = 75 - A when 38 <= A <= 60 \ 16 when A > 60 \ \ so Y is in the range 37 to 16, with higher values of A \ giving lower values of A \ \ This represents the distance between this value on the \ steering wheel and the nearest quadrant STY T \ Set T = Y (in the range 37 to 16) to pass to the \ DrawDashboardLine routine as the amount of slope error \ for each step slong the main axis LDX wheelPixels,Y \ Set X to the number of pixels that would be along the \ long axis of the line if the line went all the way to \ the centre of the wheel, given the value of Y above STX SS \ Set SS = X to pass to the DrawDashboardLine routine \ as the cumulative amount of slope error that equates \ to a pixel in the shorter axis JMP upda5 \ Jump to upda5 .upda4 \ If we get here then the indicator is not far away from \ the centre of the wheel, as A < 38 TAX \ Set X = A (in the range 0 to 37) \ \ This represents the distance between this value on the \ steering wheel and the nearest quadrant STX T \ Set T = X (in the range 0 to 37) to pass to the \ DrawDashboardLine routine as the amount of slope error \ for each step slong the main axis LDA V \ Flip bit 0 of V, to flip it from the first half of the EOR #1 \ quadrant to the second half STA V \ By this point, V has the following value, which we \ pass to the DrawDashboardLine routine \ \ * 2 when sign bit of steeringLo is set and A >= 38 \ i.e. steering left a lot \ Shallow slope, right and down \ \ * 3 when sign bit of steeringLo is set and A < 38 \ i.e. steering left a little \ Steep slope, right and down \ \ * 4 when sign bit of steeringLo is clear and A < 38 \ i.e. steering right a little \ Steep slope, left and down \ \ * 5 when sign bit of steeringLo is clear and A >= 38 \ i.e. steering right a lot \ Shallow slope, left and down \ \ These are the opposite way round to the rev counter \ hand, which is also drawn by the DrawDashboardLine \ routine - this is because the rev counter hand is \ drawn from the centre outwards, while the steering \ wheel line is drawn from the outside in LDA wheelPixels,X \ Set A to the number of pixels that would be along the \ long axis of the line if the line went all the way to \ the centre of the wheel, given the value of X above STA SS \ Set SS = A to pass to the DrawDashboardLine routine \ as the cumulative amount of slope error that equates \ to a pixel in the shorter axis .upda5 ASL A \ Set A = A * 2 + 4 CLC ADC #4 EOR #&FF \ Set Y = ~A TAY TXA \ Set A = X \ \ where X is either the original value of A (0 to 37) \ or the Y-th value of wheelPixels (where Y is 16 to \ 37), which is in the range 38 to 51 \ \ In effect, this is the horizontal distance of the \ steering line from the centre point PLP \ Set the C flag to bit 0 of steeringLo (the sign bit), \ which we stored on the stack above BCC upda6 \ If bit 0 of steeringLo is clear, we are steering to \ the right, so skip the following instruction EOR #&FF \ Set A = ~A \ = -A - 1 \ \ so the following addition becomes: \ \ A = A + 80 \ = -A - 1 + 80 \ = 79 - A .upda6 CLC \ Set A = A + 80 ADC #80 \ \ which turns A into the position on the steering wheel \ where 80 is the centre point at the top middle of the \ wheel STA W \ Set W = A, so W contains the position on the steering \ wheel AND #%11111100 \ Clear bits 0 and 1 of A, to set A = A div 4 JSR GetScreenAddress \ Set (Q P) to the screen address for pixel coordinate \ (A, Y), setting Y to the pixel row within the \ character block containing the pixel (both of which we \ pass to the DrawDashboardLine routine) LDA W \ Set W = W * 2 mod 8 ASL A \ AND #%00000111 \ This takes the x-coordinate of the line on the STA W \ steering wheel, doubled so we have 160 in the centre \ of the steering wheel (so it matches the coordinates \ in mode 5), and then we look at bits 0 to 2 only to \ get the starting pixel to pass to DrawDashboardLine LDA #%100 \ Set H = %100, so the DrawDashboardLine looks up values STA H \ from the second half of the pixelByte and yLookupLo+8 \ tables (so the line is drawn on black rather than \ white) LDA #6 \ Set U = 6, so the line contains up to seven pixels STA U JSR DrawDashboardLine \ Draw the dashboard line RTS \ Return from the subroutine
Name: DrawRevCounter [Show more] Type: Subroutine Category: Dashboard Summary: Draw the hand on the rev counter
Context: See this subroutine on its own page References: This subroutine is called as follows: * UpdateDashboard calls DrawRevCounter
.DrawRevCounter LDA #%000 \ Set H = %000 to pass into DrawDashboardLine below STA H LDA revCount \ Set A = revCount, which is the value we want to draw \ on the rev counter, in the range 0 to 170 CMP #30 \ If A >= 30, skip the following instruction BCS revs1 LDA #30 \ Set A = 30, so A is always at least 30, and is now in \ the range 30 to 170 (we do this because the hand on \ the rev counter doesn't fall all the way back to zero) .revs1 STA T \ Set T = A LSR A \ Set A = A / 2 + T CLC \ = A / 2 + A ADC T \ = 1.5 * A \ = 1.5 * revCount \ \ which is in the range 45 to 255 ROR A \ Set A = A / 2 \ = 0.75 * revCount \ \ which is in the range 22 to 127 \ We now convert the value in A to the corresponding \ position on the dial in terms of which quadrant it's \ in, and which half of that quadrant, so we can pass \ the details to the DrawDashboardLine routine SEC \ Set A = A - 76 SBC #76 BCS revs2 \ If the subtraction went past zero, add 152, to get: ADC #152 \ \ A = A + 76 \ So for A in the range 22 to 127, this converts: \ \ A = 22-75 into A = 98-151 \ A = 76-127 into A = 0-51 \ \ If we consider a clock with 0 at 12 o'clock, then 38 \ at 3 o'clock, 76 at 6 o'clock and 114 at 9 o'clock, \ A is now the position of the hand on that clock, i.e. \ the position of the hand that we want to draw on the \ rev counter .revs2 \ We now calculate the quadrant that contains the hand \ on the rev counter, numbered 0 to 3 counting clockwise \ from top-right \ \ We do this by calculating X = A / 38, by repeatedly \ subtracting 38 from A until we go past zero LDX #&FF \ We start by setting X = -1 SEC \ Set the C flag for the subtraction .revs3 INX \ Increment X as we are doing a subtraction SBC #38 \ Set A = A - 38 BCS revs3 \ If the subtraction didn't take us past zero, loop back \ to subtract another 38 ADC #38 \ Otherwise add the 38 back that pushed us over the \ limit, so X now contains the quadrant number, and A \ contains the remainder (i.e. the fraction that the \ hand is past the start of the quadrant) CMP #19 \ If the remainder is < 19, skip the following, as A BCC revs4 \ contains the distance from the start of quadrant X to \ the position of the hand (and the C flag is clear) SBC #19 \ Set A = ~(A - 19) + 20 EOR #&FF \ = ~(A - 19) + 1 + 19 CLC \ = -(A - 19) + 19 ADC #20 \ = 19 - (A - 19) \ \ so A now contains the distance from the hand to the \ end of quadrant X SEC \ Set the C flag to indicate that A is now the distance \ from the hand to the end of the quadrant .revs4 \ By this point: \ \ X = quadrant number (0 to 3) \ \ A = distance from start of quadrant to hand (C = 0) \ distance from hand to end of quadrant (C = 1) \ \ where each quadrant is 38 in size, so A is <= 19 \ \ The C flag therefore represents which half of the \ quadrant the hand is in, 0 denoting the first half and \ 1 denoting the second half TAY \ Set Y = the distance between the hand and quadrant STY T \ Set T = the distance between the hand and quadrant TXA \ Ensure X is in the range 0 to 3 (it should be, but AND #3 \ this makes absolutely sure) TAX TXA \ This sets bits 1 and 2 of V to the quadrant number, ROL A \ and bit 0 to the C flag, so the possible values are: STA V \ \ * 0 = %000 = Quadrant 0, first half, 12:00 to 1:30 \ * 1 = %001 = Quadrant 0, second half, 1:30 to 3:00 \ * 2 = %010 = Quadrant 1, first half, 3:00 to 4:30 \ * 3 = %011 = Quadrant 1, second half, 4:30 to 6:00 \ * 4 = %100 = Quadrant 2, first half, 6:00 to 7:30 \ * 5 = %101 = Quadrant 2, second half, 7:30 to 9:00 \ * 6 = %110 = Quadrant 3, first half, 9:00 to 10:30 \ * 7 = %111 = Quadrant 3, second half, 10:30 to 12:00 \ \ These are the quadrant values we need to pass to the \ DrawDashboardLine routine below AND #%11111100 \ If bit 2 of A is zero, then the hand is in the right BEQ revs5 \ half of the dial, so jump to revs5 to set W = 0 LDA #7 \ Otherwise the hand is in the left half of the dial, \ so set A so we set W = 7 below .revs5 STA W \ Set W = 0 if the hand is in the right half \ 7 if the hand is in the left half \ \ so we start drawing from the leftmost pixel when \ drawing to the right, or the rightmost pixel when \ drawing to the left (which ensures that the hand joins \ the centre spoke of the rev counter without a gap) LDA handPixels,Y \ Set A to the number of pixels that are along the long \ axis of the hand, given the distance between the hand \ and quadrant that we set in Y above STA SS \ Set SS to the number of pixels along the long axis STA U \ Set U to the number of pixels along the long axis, to \ pass through to the DrawDashboardLine routine below LDA startDialLo,X \ Set the low byte of (Q P) to the low byte of the AND #%11111000 \ screen address for the starting point of the hand for STA P \ quadrant Y, which we get from the startDialLo table, \ and clear bits 0 to 2 so the address points to the \ top line of the relevant character block LDA startDialLo,X \ Set Y to the pixel row within the character block AND #%00000111 \ for the starting point, which we get from bits 0 to 2 TAY \ of the starting point's screen address LDA startDialHi,X \ Set the high byte of (Q P) to the high byte of the STA Q \ screen address for the starting point of the hand for \ quadrant Y, so (Q P) now contains the full address of \ the starting point's character block \ Fall through into DrawDashboardLine to draw a line \ from the starting point given in (Q P) and Y, in the \ direction given in V, with U pixels along the longest \ axis, and in the half of the dial given in W
Name: DrawDashboardLine [Show more] Type: Subroutine Category: Dashboard Summary: Draw a hand on the rev counter or a line on the steering wheel
Context: See this subroutine on its own page References: This subroutine is called as follows: * UpdateDashboard calls DrawDashboardLine

This routine is a mode 5 Bresenham line-drawing routine, which modifies itself to cater for lines of different slopes. Arguments: V The slope of the line to draw, which is expressed as a quadrant number, where quadrant 0 is from 12 o'clock to 3 o'clock, with quadrants ordered 0 to 3 in a clockwise order: * 0 = %000 = Quadrant 0, first half (12:00 to 1:30) Steep slope, right and up Step up along y-axis (stepAxis = DEY) Move right along x-axis (shortAxis = INX) * 1 = %001 = Quadrant 0, second half (1:30 to 3:00) Shallow slope, right and up Step right along x-axis (stepAxis = INX) Move up along y-axis (shortAxis = DEY) * 2 = %010 = Quadrant 1, first half (3:00 to 4:30) Shallow slope, right and down Step right along x-axis (stepAxis = INX) Move down along y-axis (shortAxis = INY) * 3 = %011 = Quadrant 1, second half (4:30 to 6:00) Steep slope, right and down Step down along y-axis (stepAxis = INY) Move right along x-axis (shortAxis = INX) * 4 = %100 = Quadrant 2, first half (6:00 to 7:30) Steep slope, left and down Step down along y-axis (stepAxis = INY) Move left along x-axis (shortAxis = DEX) * 5 = %101 = Quadrant 2, second half (7:30 to 9:00) Shallow slope, left and down Step left along x-axis (stepAxis = DEX) Move down along y-axis (shortAxis = INY) * 6 = %110 = Quadrant 3, first half (9:00 to 10:30) Shallow slope, left and up Step left along x-axis (stepAxis = DEX) Move up along y-axis (shortAxis = DEY) * 7 = %111 = Quadrant 3, second half (10:30 to 12:00) Steep slope, left and up Step up along y-axis (stepAxis = DEY) Move left along x-axis (shortAxis = DEX) (Q P) The screen address of the character block containing the line's starting point Y The pixel row within the character block containing the line's starting point (0 to 7) U The number of pixels to step along the step axis: * The number of pixels along the longer (step) axis when drawing the rev counter * 6 when drawing the steering wheel line T The slope error for each step along the step axis is T/SS, and T is set to: * The distance between the hand and quadrant when drawing the rev counter hand * When drawing the steering wheel line: 0-37 when the line is close to the centre (in the top two quadrants either side of the centre) 37-16 when line is further from the centre (in the left and right quadrants) SS The slope error for each step along the step axis is T/SS, and SS is set to: * The same as U when drawing the rev counter hand i.e. the number of pixels along the longer (step) axis * A value from wheelPixels when drawing the steering wheel line (38 to 53) H The starting index to use in the pixelByte and yLookupLo+8 lookup tables: * %000 when drawing the rev counter hand, so the line gets drawn in white * %100 when drawing the steering wheel line, so the line gets drawn in black W The pixel number (0-7) of the first pixel to draw along the x-axis
.DrawDashboardLine LDX V \ Modify the instruction at dlin2 to the V-th shortAxis LDA shortAxis,X \ instruction STA dlin2 LDA stepAxis,X \ Modify the instruction at dlin8 to the V-th stepAxis STA dlin8 \ instruction \ The following code has the instructions for V = %010, \ which has INY at dlin2 for the short axis, and INX at \ dlin8 for the step axis, so that's this kind of line: \ \ * Quadrant 1, first half (3:00 to 4:30) \ * Shallow slope, right and down \ * Step right along x-axis (stepAxis = INX) \ * Move down along y-axis (shortAxis = INY) LDX W \ Set X = W, so X contains the position of the current \ pixel within the pixel row, if there were eight pixels \ per row LDA #0 \ Set A = -SS SEC \ SBC SS \ So this is the starting point for our slope error \ calculation CLC \ Clear the C flag for the following addition .dlin1 \ We use A to keep track of the slope error, adding the \ step along the smaller axis (in T) until it reaches 0, \ at which point it is a multiple of SS and we need \ to move one pixel along the smaller axis ADC T \ Set A = A + T \ \ So A is updated with the slope error BCC dlin3 \ If the addition didn't overflow, then the result in A \ is still negative, so skip the following instruction \ The slope error just overflowed (in other words, the \ cumulative slope error in A just reached a multiple of \ SS), so we need to adjust the slope error to make \ it negative again, and we need to step along the \ shorter axis SBC SS \ Subtract SS from the cumulative slope error to \ bring it back to being negative, so we can detect when \ it reaches next multiple of SS .dlin2 INY \ Increment Y to move down along the y-axis (i.e. along \ the shorter axis) \ \ This instruction is modified at the start of this \ routine, depending on the slope of the line in V .dlin3 STA II \ Store the updated slope error in II, so we can \ retrieve it below, ready for the next iteration of the \ drawing loop TXA \ X contains the position of the current pixel within LSR A \ the pixel row, in the range 0 to 7, so set A to half AND #%00000011 \ this value to get the mode 5 pixel number (as there \ are only four pixels per pixel byte on mode 5) ORA H \ Set bit 2 of A if this is the steering wheel, which \ is the same as adding 4 STA V \ Store the result in V, so V contains the pixel number \ (0 to 3) of the pixel to draw, plus 4 if this is the \ steering wheel (4 to 7) TXA \ X contains the position of the current pixel within \ the pixel line, so put this in A BPL dlin4 \ If bit 7 of A is clear, jump to dlin4 \ Otherwise we need to move (Q P) to the previous \ character block to the left, by subtracting 8 (as \ there are 8 bytes per character block) LDX #7 \ Set X = 7 to set as the new value of W below LDA P \ Set (Q P) = (Q P) - 8 SEC \ SBC #8 \ starting with the low bytes STA P BCS dlin5 \ And then the high bytes DEC Q BCS dlin5 \ This instruction has no effect, as we already passed \ through the BCS above, which is presumably a bug (this \ should perhaps be a BCC?) .dlin4 CMP #8 \ If A < 8, jump to dlin5 BCC dlin5 \ Otherwise we need to move (Q P) to the next character \ block to the right, by adding 8 (as there are 8 bytes \ per character block) LDX #0 \ Set X = 0 to set as the new value of W below LDA P \ Set (Q P) = (Q P) + 8 CLC \ ADC #8 \ starting with the low bytes STA P BCC dlin5 \ And then the high bytes INC Q .dlin5 STX W \ Store X in W, so W moves along one pixel to the right LDX lineBufferSize \ Set X to the size of the line buffer, which gives us \ the index of the next empty space in the buffer TYA \ Y contains the number of the pixel row within the \ current character block, so put this in A BPL dlin6 \ If A >=0, jump to dlin6 \ Otherwise we need to move (Q P) to the next character \ row above, by subtracting &140 (as there are &140 \ bytes per character row) LDA P \ Set (Q P) = (Q P) - &140 SEC \ SBC #&40 \ starting with the low bytes STA P LDA Q \ And then the high bytes SBC #&01 STA Q LDY #7 \ Set Y = A = 7 as the new value of Y TYA BNE dlin7 \ Jump to dlin7 (this BNE is effectively a JMP as A is \ never zero) .dlin6 CMP #8 \ If A < 8, jump to dlin7 BCC dlin7 \ Otherwise we need to move (Q P) to the next character \ row below, by adding &140 (as there are &140 bytes per \ character row) LDA P \ Set (Q P) = (Q P) + &140 CLC \ ADC #&40 \ starting with the low bytes STA P LDA Q \ And then the high bytes ADC #&01 STA Q LDY #0 \ Set Y = A = 0 as the new value of Y TYA .dlin7 \ We now store the details of the pixel we are about \ to overwrite in the line buffer, which stores a screen \ address plus the original contents of that address \ \ We get the screen address by adding the address of the \ character block in P to the number of the pixel row \ within the character in A, which we can do with an ORA \ as P only occupies bits 3 to 7, while A only occupies \ bits 0 to 2 ORA P \ Store the address we are about to overwrite in the STA lineBufferAddrLo,X \ next empty space at the end of the line buffer, i.e. \ the X-th byte of (lineBufferAddrHi lineBufferAddrLo), \ starting with the low byte of the address LDA Q \ And then the high byte of the address STA lineBufferAddrHi,X LDA (P),Y \ Store the current pixel contents into the pixel STA lineBufferPixel,X \ contents buffer at lineBufferPixel INC lineBufferSize \ Increment the size of the pixel buffers, as we just \ added an entry LDX V \ Set X = V, so X now contains the pixel number \ (0 to 3) of the pixel to draw, plus 4 if this is the \ steering wheel (4 to 7) AND yLookupLo+8,X \ Apply the X-th pixel mask from yLookupLo+8, so this \ clears the X-th pixel in the pixel row (the table \ contains the same bytes in 0 to 3 as in 0 to 7) ORA pixelByte,X \ OR with a pixel byte with pixel X set, so this sets \ the X-th pixel to colour 2 (white) if X is 0 to 3, or \ colour 0 (black) if X is 4 to 7 - so the rev counter \ hand is white, while the steering wheel line is black STA (P),Y \ Draw the pixel byte to the screen \ We now set up all the variables so we can loop back \ to dlin1 for the next pixel LDX W \ Set X = W LDA II \ Set A to the current slope error, which we stored in \ II above CLC \ Clear the C flag for the addition at the start of the \ loop .dlin8 INX \ Increment X to step right along the x-axis (i.e. along \ the longer axis) \ \ This instruction is modified at the start of this \ routine, depending on the slope of the line in V DEC U \ Decrement the pixel counter BMI dlin9 \ If we have drawn the correct number of pixels along \ the longer axis, jump to dlin9 to return from the \ subroutine as we have finished drawing the line JMP dlin1 \ Otherwise loop back to draw the next pixel .dlin9 RTS \ Return from the subroutine
Name: AnimateTyres [Show more] Type: Subroutine Category: Dashboard Summary: Update screen memory to animate the tyres
Context: See this subroutine on its own page References: This subroutine is called as follows: * ScreenHandler calls AnimateTyres
.AnimateTyres INC irqCounter \ Increment irqCounter, so it gets incremented every \ time the IRQ routine reaches section 4 of the custom \ screen LDA playerSpeedHi \ Set tyreTravel = tyreTravel + playerSpeedHi + 48 CLC ADC #48 ADC tyreTravel STA tyreTravel BCC tyre4 \ If the addition didn't overflow, jump to tyre4 to \ return from the subroutine LDA playerMoving \ If playerMoving = 0 then the player's car is not BEQ tyre4 \ moving so we don't need to animate the tyres, so jump \ to tyre4 to return from the subroutine LDX #4 \ Set a loop counter to go from 4 to 0 .tyre1 LDA tyreLeft3,X \ Set tyreLeft3 = tyreLeft3 EOR tyreTreadLeft EOR tyreTreadLeft,X STA tyreLeft3,X LDA tyreRight3,X \ Set tyreRight3 = tyreRight3 EOR tyreTreadRight EOR tyreTreadRight,X STA tyreRight3,X CPX #3 \ If X >= 3, jump to tyre2 to skip the following BCS tyre2 LDA tyreLeft1,X \ Flip the top four bits of tyreLeft1 EOR #%11110000 STA tyreLeft1,X LDA tyreRight2,X \ Flip the top four bits of tyreRight2 EOR #%11110000 STA tyreRight2,X BNE tyre3 \ If A is non-zero, jump to tyre3 to continue the loop .tyre2 LDA tyreLeft2,X \ Flip bits 6 and 7 of tyreLeft2 EOR #%11000000 STA tyreLeft2,X LDA tyreRight1,X \ Flip bits 4 and 5 of tyreRight1 EOR #%00110000 STA tyreRight1,X .tyre3 DEX \ Decrement the loop counter BPL tyre1 \ Loop back to animate the next tyre part .tyre4 RTS \ Return from the subroutine
Name: tyreTreadLeft [Show more] Type: Variable Category: Dashboard Summary: Tyre tread pattern for the left tyre
Context: See this variable on its own page References: This variable is used as follows: * AnimateTyres calls tyreTreadLeft
.tyreTreadLeft EQUB %11110000 EQUB %11110000 EQUB %11000000 EQUB %11000000 EQUB %10000000
Name: tyreTreadRight [Show more] Type: Variable Category: Dashboard Summary: Tyre tread pattern for the right tyre
Context: See this variable on its own page References: This variable is used as follows: * AnimateTyres calls tyreTreadRight
.tyreTreadRight EQUB %11110000 EQUB %11110000 EQUB %00110000 EQUB %00110000 EQUB %00010000
Name: trackData [Show more] Type: Workspace Category: Track data Summary: This is where the track data gets loaded Deep dive: The jigsaw puzzle binary The track data file format
Context: See this workspace on its own page References: This workspace is used as follows: * SwapCode calls trackData

See the track source in revs-silverstone.asm for details of the track data. It covers trackData and dashData41 - the latter gets moved into screen memory as part of the memory-moving process in the SwapData routine.
.trackData .trackSectionData SKIP 1 \ Various data for the track section: \ \ * Bits 0-2: Size of the track section list \ \ * Bits 4-7: Sign number .xTrackSectionIHi SKIP 1 \ High byte of the x-coordinate of the starting point of \ the inner verge of each track section .yTrackSectionIHi SKIP 1 \ High byte of the y-coordinate of the starting point of \ the inner verge of each track section .zTrackSectionIHi SKIP 1 \ High byte of the z-coordinate of the starting point of \ the inner verge of each track section .xTrackSectionOHi SKIP 1 \ High byte of the x-coordinate of the starting point of \ the outside verge of each track section .trackSectionTurn SKIP 1 \ The number of the segment in the section where \ non-player drivers should start turning in preparation \ for the next section .zTrackSectionOHi SKIP 1 \ High byte of the z-coordinate of the starting point of \ the outside verge of each track section .trackDriverSpeed SKIP 1 \ The maximum speed for non-player drivers on this \ section of the track SKIP 8 * 25 \ Section data for 25 more sections .xTrackSignVector SKIP 16 \ The x-coordinate of the track sign vector for each \ sign, to be scaled and added to the inner track \ section vector for the sign .zTrackSignVector SKIP 16 \ The z-coordinate of the track sign vector for each \ sign, to be scaled and added to the inner track \ section vector for the sign .yTrackSignVector SKIP 16 \ The y-coordinate of the track sign vector for each \ sign, to be scaled and added to the inner track \ section vector for the sign .xTrackSegmentI SKIP 256 \ Vector x-coordinates between two consecutive segments \ on the inside of the track .yTrackSegmentI SKIP 256 \ Vector y-coordinates between two consecutive segments \ on the inside of the track .zTrackSegmentI SKIP 256 \ Vector z-coordinates between two consecutive segments \ on the inside of the track .xTrackSegmentO SKIP 256 \ Vector x-coordinates from the inside to the outside of \ the track for each segment .zTrackSegmentO SKIP 256 \ Vector z-coordinates from the inside to the outside of \ the track for each segment .trackSectionFlag SKIP 1 \ Various flags for the track section: \ \ * Bit 0: Section shape (Sh) \ \ * 0 = straight section (only one segment vector) \ \ * 1 = curved section (multiple segment vectors) \ \ * Bit 1: Colour of left verge marks (Vcol) \ \ * 0 = black-and-white verge marks \ \ * 1 = red-and-white verge marks \ \ * Bit 2: Colour of right verge marks (Vcol) \ \ * 0 = black-and-white verge marks \ \ * 1 = red-and-white verge marks \ \ * Bit 3: Show corner markers on right (Mlr) \ \ * 0 = do not show corner markers to the right of \ the track \ \ * 1 = show corner markers to the right of the \ track \ \ * Bit 4: Show corner markers on left (Mlr) \ \ * 0 = do not show corner markers to the left of \ the track \ \ * 1 = show corner markers to the left of the track \ \ * Bit 5: Corner marker colours (Mcol) \ \ * 0 = show all corner markers in white \ \ * 1 = show corner markers in red or white, as \ appropriate \ \ * Bit 6: In the extra tracks only, nable hooks to \ generate segment vectors (G) \ \ * 0 = disable HookDataPointers and \ HookSegmentVector \ \ * 1 = enable HookDataPointers and \ HookSegmentVector \ \ * Bit 7: Section has a maximum speed (Sp) \ \ * 0 = this section has no maximum speed \ \ * 1 = this section has a maximum speed .xTrackSectionILo SKIP 1 \ Low byte of the x-coordinate of the starting point of \ the inner verge of each track section .yTrackSectionILo SKIP 1 \ Low byte of the y-coordinate of the starting point of \ the inner verge of each track section .zTrackSectionILo SKIP 1 \ Low byte of the z-coordinate of the starting point of \ the inner verge of each track section .xTrackSectionOLo SKIP 1 \ Low byte of the x-coordinate of the starting point of \ the outside verge of each track section .trackSectionFrom SKIP 1 \ The number of the first segment vector in each \ section, which enables us to fetch the segment vectors \ for a given track section .zTrackSectionOLo SKIP 1 \ Low byte of the z-coordinate of the starting point of \ the outside verge of each track section .trackSectionSize SKIP 1 \ The length of each track section in terms of segments SKIP 8 * 25 \ Section data for 25 more sections .trackSteering SKIP 24 \ The optimum steering for non-player drivers to apply \ on each track section SKIP 2 .trackSignData SKIP 16 \ Base coordinates and object types for 16 road signs .trackSectionCount SKIP 1 \ The total number of track sections * 8 .trackVectorCount SKIP 1 \ The total number of segment vectors in the segment \ vector tables .trackLength SKIP 2 \ The length of the full track in terms of segments .trackStartLine SKIP 2 \ The segment number of the starting line .trackLapTimeSec SKIP 3 \ Lap times for adjusting the race class (seconds) .trackLapTimeMin SKIP 3 \ Lap times for adjusting the race class (minutes) .trackGearRatio SKIP 7 \ The gear ratio for each gear .trackGearPower SKIP 7 \ The power for each gear .trackBaseSpeed SKIP 3 \ The base speed for each race class, used when \ generating the best racing lines and non-player driver \ speeds .trackStartPosition SKIP 1 \ The starting race position of the player during a \ practice or qualifying lap .trackCarSpacing SKIP 1 \ The spacing between the cars at the start of a \ qualifying lap, in segments .trackTimerAdjust SKIP 1 \ Adjustment factor for the speed of the timers to allow \ for fine-tuning of time on a per-track basis .trackRaceSlowdown SKIP 1 \ Slowdown factor for non-player drivers in the race SKIP 7
Name: dashData41 [Show more] Type: Variable Category: Screen buffer Summary: Contains part of the dashboard image that gets moved into screen memory Deep dive: The jigsaw puzzle binary
Context: See this variable on its own page References: No direct references to this variable in this source file
CLEAR &594A, &5A22 \ The track data is loaded in a separate file that is ORG &594A \ moved to trackData after the game binary has loaded \ \ It overwrites part of the dashboard image at that's \ loaded as part of the main game binary at dashdata41, \ which is moved into screen memory before the track \ data is moved \ \ These lines rewind BeebAsm's assembly back to \ dashData41 (which is at address &594A), and clear the \ block from that point to CallTrackHook, so we can \ can set the correct address for dashData41 while also \ retaining the addresses we just set up for the track \ data .dashData41 SKIP 67 SKIP 149
Name: CallTrackHook [Show more] Type: Subroutine Category: Setup Summary: The track file's hook code Deep dive: The track data file format
Context: See this subroutine on its own page References: This subroutine is called as follows: * SetupGame calls CallTrackHook
.CallTrackHook BRK \ The SwapCode routine replaces these three bytes with BRK \ the three bytes from just before the trackChecksum in BRK \ the track file, which contain the three bytes of hook \ code for the track \ \ In the default Silverstone track that comes with the \ original version of Revs, the three bytes of hook code \ contain the following: \ \ RTS \ NOP \ NOP \ \ so calling this routine does nothing (see the track \ source in the revs-silverstone.asm file for details) \ \ Other track files, such as those in the Revs 4 Tracks \ expansion pack, contain JMP instructions in their hook \ code, which allows the track authors to hook in entire \ routines that get called when those tracks are loaded
Name: AwardRacePoints [Show more] Type: Subroutine Category: Drivers Summary: Award points following a race
Context: See this subroutine on its own page References: This subroutine is called as follows: * MainLoop (Part 6 of 6) calls AwardRacePoints

This routine awards points to a driver for finishing in the top six in a race, or for getting the fastest lap time. The points awarded are based on the driver's race position, as per the pointsForPlace table: * 9 points for first place * 6 points for second place * 4 points for third place * 3 points for fourth place * 2 points for fifth place * 1 point for sixth place * 1 point for the fastest lap In single-player races, the points are awarded as above. In multi-player races, an algorithm is used to share out the points in a way that takes the relative skills into consideration. Specifically, the routine awards this many points: (U T) * the points from the above list This is how (U T) is calculated: * Single-player race: (U T) = numberOfPlayers = 1, so we award the amount of points shown above * Multi-player race: If we are awarding points to the current player: (U T) = (numberOfPlayers - 1) * numberOfPlayers If we are awarding points to a human player but not the current player: (U T) = numberOfPlayers If we are awarding points to a computer driver: (U T) = (numberOfPlayers - 1) * 2 I have no idea why the algorithm works like this. It needs more analysis! Arguments: X The race position to award points to: * 0 to 5 for the first six places * 6 for the fastest lap
.AwardRacePoints LDA #0 \ Zero the points in (racePointsHi racePointsLo) for STA racePointsLo,X \ race position X STA racePointsHi,X STA U \ Set U = 0, to act as the high byte of (U T) LDY driversInOrder,X \ Set Y to the number of the driver in race position X CPX #6 \ If we called the routine with X = 0 to 5, then jump to BNE poin1 \ poin1 to skip the following instruction LDY driversInOrder \ We called the routine with X = 6, so set Y to the \ winning driver's number, i.e. the driver with the \ fastest lap .poin1 \ By this point, Y contains the number of the driver we \ want to give the points to, so now we calculate the \ number of points to award LDA numberOfPlayers \ Set A to the number of players - 1 SEC SBC #1 BEQ poin3 \ If A = 0 then there is only one player, so jump to \ poin3 to skip the following CPY currentPlayer \ If Y is the number of the current player, jump to BEQ poin2 \ poin2 CPY lowestPlayerNumber \ If Y >= lowestPlayerNumber then this is a human BCS poin3 \ player but not the current player, so jump to poin3 \ If we get here then we are awarding points to a \ computer-controlled driver ASL A \ Double the value of A, to use as the value of T, so \ we will get: \ \ (U T) = (0 T) \ = T \ = A * 2 \ = (numberOfPlayers - 1) * 2 BNE poin4 \ Jump to poin4 (this BNE is effectively a JMP, as A is \ never zero) .poin2 \ If we get here then we are awarding points to the \ current player STA U \ Set U = A = numberOfPlayers - 1 LDA numberOfPlayers \ Set A = numberOfPlayers JSR Multiply8x8 \ Set (A T) = A * U \ = (numberOfPlayers - 1) * numberOfPlayers STA U \ Set (U T) = (A T) \ = (numberOfPlayers - 1) * numberOfPlayers JMP poin5 \ Jump to poin5 .poin3 \ If we get here then either there is only one player, \ or we are awarding points to a human player but not \ the current player LDA numberOfPlayers \ Set A to the number of players, to use as the value of \ T, so we will get: \ \ (U T) = (0 T) \ = (0 numberOfPlayers) \ = numberOfPlayers BNE poin4 \ This instruction has no effect as poin4 is the next \ instruction anyway .poin4 STA T \ Store A in T, so this sets (U T) = (U A) .poin5 SED \ Set the D flag to switch arithmetic to Binary Coded \ Decimal (BCD) \ We now do the following addition 256 * U + T times, so \ the total number of points added is: \ \ (256 * U + T) * (9, 6, 4, 3, 2 or 1) \ \ or putting it another way: \ \ (U T) * (9, 6, 4, 3, 2 or 1) .poin6 LDA pointsForPlace,X \ Add the X-th entry in pointsForPlace to the X-th entry CLC \ in (racePointsHi racePointsLo), starting with the low ADC racePointsLo,X \ bytes STA racePointsLo,X LDA racePointsHi,X \ And then the high bytes ADC #0 STA racePointsHi,X DEC T \ Decrement the counter in T BNE poin6 \ Loop back to poin6 so we do the addition a total of T \ times DEC U \ Decrement the counter in U BPL poin6 \ Loop back to poin6 so we do an additional U loops, \ with the inner loop repeating 256 times as T is now 0, \ so this does a total of 256 * U additional additions JSR AddRacePoints \ Add the race points from above to the accumulated \ points for driver Y RTS \ Return from the subroutine