\ ****************************************************************************** \ \ REVS SOURCE \ \ Revs was written by Geoffrey J Crammond and is copyright Acornsoft 1985 \ \ The code on this site has been reconstructed from a disassembly of the \ original game binaries \ \ The commentary is copyright Mark Moxon, and any misunderstandings or mistakes \ in the documentation are entirely my fault \ \ The terminology and notations used in this commentary are explained at \ https://revs.bbcelite.com/terminology \ \ The deep dive articles referred to in this commentary can be found at \ https://revs.bbcelite.com/deep_dives \ \ ------------------------------------------------------------------------------ \ \ This source file produces the following binary file: \ \ * Revs2.bin \ \ ****************************************************************************** INCLUDE "1-source-files/main-sources/revs-build-options.asm" _ACORNSOFT = (_VARIANT = 1) _SUPERIOR = (_VARIANT = 2) _4TRACKS = (_VARIANT = 3) _REVSPLUS = (_VARIANT = 4) GUARD &8000 \ Guard against assembling over sideways ROMs \ ****************************************************************************** \ \ Configuration variables \ \ ****************************************************************************** IRQ1V = &0204 \ The IRQ1V vector that we intercept to implement the \ screen mode VIA = &FE00 \ Memory-mapped space for accessing internal hardware, \ such as the video ULA, 6845 CRTC and 6522 VIAs (also \ known as SHEILA) OSRDCH = &FFE0 \ The address for the OSRDCH routine OSWRCH = &FFEE \ The address for the OSWRCH routine OSBYTE = &FFF4 \ The address for the OSBYTE routine OSWORD = &FFF1 \ The address for the OSWORD routine CODE% = &0B00 \ The address of the main game code LOAD% = &1200 \ The load address of the main code binary LOAD_END% = &7000 \ The address of the end of the main code binary dashData = &3000 \ The address of the first code block that gets swapped \ in and out of screen memory, along with parts of the \ dashboard image trackLoad = &70DB \ The load address of the track data file trackChecksum = &7800 \ The address of the checksums in the track data file \ after it is loaded but before it is moved in memory \ The following configuration variables represent screen \ addresses for the custom screen tyreLeft1 = &6E85 \ The tread on the left tyre in screen memory (part 1) tyreLeft2 = &6E8A \ The tread on the left tyre in screen memory (part 2) tyreLeft3 = &6FC0 \ The tread on the left tyre in screen memory (part 3) tyreRight1 = &6FBD \ The tread on the right tyre in screen memory (part 1) tyreRight2 = &6FB2 \ The tread on the right tyre in screen memory (part 2) tyreRight3 = &70F8 \ The tread on the right tyre in screen memory (part 3) leftSurface = &713D \ A point in the track view next to the left edge of the \ dashboard, which we use to determine the surface under \ the left side of the car rightSurface = &7205 \ A point in the track view next to the right edge of \ the dashboard, which we use to determine the surface \ under the left right of the car mirror0 = &7540 \ Mirror 0 base address (left mirror, outer segment) mirror1 = &7548 \ Mirror 1 base address (left mirror, middle segment) mirror2 = &7418 \ Mirror 2 base address (left mirror, inner segment) mirror3 = &7530 \ Mirror 3 base address (right mirror, inner segment) mirror4 = &7670 \ Mirror 4 base address (right mirror, middle segment) mirror5 = &7678 \ Mirror 5 base address (right mirror, outer segment) assistLeft1 = &77DB \ Centre-bottom of dashboard in screen memory for \ showing the computer assisted steering (CAS) \ indicator (part 1) assistLeft2 = &77DC \ Centre-bottom of dashboard in screen memory for \ showing the computer assisted steering (CAS) \ indicator (part 2) assistRight1 = &77E3 \ Centre-bottom of dashboard in screen memory for \ showing the computer assisted steering (CAS) \ indicator (part 3) assistRight2 = &77E4 \ Centre-bottom of dashboard in screen memory for \ showing the computer assisted steering (CAS) \ indicator (part 4) \ The following configuration variables represent screen \ addresses for mode 7 row2_column1 = &7C79 \ Chequered flag mode 7 screen address row18_column5 = &7E85 \ The first entry's number in a mode 7 menu row24_column5 = &7FC5 \ Location of "PRESS SPACE BAR TO CONTINUE" prompt \ ****************************************************************************** \ \ Name: Zero page \ Type: Workspace \ Address: &0070 to &008F \ Category: Workspaces \ Summary: Mainly temporary variables that are used a lot \ \ ****************************************************************************** ORG &0000 .playerMoving SKIP 1 \ Flag to denote whether the player's car is moving \ \ * 0 = player's car is not moving \ \ * Non-zero = player's car is moving .thisSectionFlags SKIP 1 \ The track section flags for the current track section .thisVectorNumber SKIP 1 \ The number of the segment vector for the current track \ segment, ready to store in the track segment buffer \ as segmentVector .currentPosition SKIP 1 \ The position of the current player \ \ This refers to the current player's position in the \ driversInOrder list .thisSectionNumber SKIP 1 \ The number * 8 of the current section number when \ calculating the track verges .sectionListStart SKIP 1 \ The start index of the track section list that's \ used when calculating the positions of the track \ verges \ \ The list runs from index sectionListStart up to 6 .sectionListValid SKIP 1 \ The index of the first valid entry in the track \ section list \ \ The list runs from index sectionListStart up to 6, and \ valid entries run from index sectionListValid up to 6 \ \ Set to 6 in ResetVariables to indicate that the list \ does not yet contain any valid entries .sectionListSize SKIP 1 \ The size of the track section list that's used when \ calculating the positions of the track verges .sectionListPointer SKIP 1 \ The index of the current entry in the track section \ list, as we only update one entry in the list on each \ iteration of the main driving loop \ \ The list runs from index sectionListStart up to 6 \ \ Set to 6 in ResetVariables .oddsOfEngineStart SKIP 1 \ The chances of the engine starting while revving the \ engine (the engine will start with a chance of 1 in \ oddsOfEngineStart) \ \ Set to 7 in ResetVariables .playerYawAngleLo SKIP 1 \ Low byte of the player's yaw angle \ \ This is the left-right rotation of the player's car \ \ Stored as a 16-bit value (playerYawAngleHi \ playerYawAngleLo) .playerYawAngleHi SKIP 1 \ High byte of the player's yaw angle \ \ This is the left-right rotation of the player's car \ \ Stored as a 16-bit value (playerYawAngleHi \ playerYawAngleLo) .vectorNumber SKIP 1 \ The segment vector number when building car objects \ in the BuildCarObjects routine .playerPitchAngle SKIP 1 \ The player's pitch angle \ \ This is the up-down rotation of the player's car .segmentOffset SKIP 1 \ The offset to use for the segment being processed in \ the GetSegmentAngles routine \ \ * 0 when our car is facing in the same direction \ \ * 120 when our car is facing the opposite direction .leaveTrackTimer SKIP 1 \ The leave track timer \ \ When set to a non-zero figure, the timer starts to \ count down by one every iteration of the main loop, \ until it reaches 1, at which point we leave the track \ \ During the countdown we are unable to accelerate or \ brake, but can steer .edgeDistanceLo SKIP 1 \ Low byte of the distance between the player's car and \ the nearest track edge \ \ Stored as a 16-bit value (edgeDistanceHi \ edgeDistanceLo) .edgeDistanceHi SKIP 1 \ High byte of the distance between the player's car and \ the nearest track edge \ \ Stored as a 16-bit value (edgeDistanceHi \ edgeDistanceLo) .segmentListPointer SKIP 1 \ The index of the current entry in the track segment \ list \ \ As the segment list for the left side of the track is \ populated after the right side, this gives us the \ index of the last entry for the left side of the track \ once it has been populated \ \ segmentListRight gives us the equivalent index for the \ right side of the track \ \ The list runs from index 6 upwards .edgeSegmentNumber SKIP 1 \ The number of the segment within the track segment \ list that is closest to the player's car .prevSegmentOffset SKIP 1 \ The segment coordinate offset for the previous segment \ when adding segments to the track segment list .segmentListRight SKIP 1 \ The index of the last entry in the track segment \ list for the right side of the track \ \ segmentListPointer gives us the equivalent index for \ the left side of the track .previousSteering SKIP 1 \ The previous value of sectionSteering when calculating \ the optimum steering for a track segment .turnCounter SKIP 1 \ A counter for the length of turn when calculating the \ optimum steering for a track segment .prevDriverSpeed06 SKIP 1 \ Bits 0-6 of the previous value of trackDriverSpeed \ when calculating the optimum steering for a track \ segment .gearChange SKIP 1 \ Used to store the direction of the gear change \ \ * 1 = change up \ \ * 0 = no gear change \ \ * -1 = change down .debugSpinning SKIP 1 \ This variable is never read, but is set to 64 and back \ to 0 when the car spins past the point where it \ changes the direction that it is facing along the \ track \ \ Perhaps it was used for debugging the spinning \ routines? .thisPitchIndex SKIP 0 \ The index of the pitch angle in the track segment list \ for the verge we are processing .thisObjectIndex SKIP 0 \ The index of the current object part's data as we work \ our way through an object's constituent parts .rowCounter SKIP 1 \ The table row number when printing the driver tables .pressingShiftArrow SKIP 1 \ Bit 7 is set if we are pressing SHIFT and right arrow \ (which restarts the game) .thisPosition SKIP 1 \ The position of the car that we are analysing in the \ MoveAndDrawCars routine .vergeOnScreenEdge SKIP 1 \ Determines whether the verge edge we are drawing for \ the current segment is partially off-screen \ \ * 0 = Edge is either wholly on-screen or off-screen \ \ * &FF = Edge is partially off-screen .horizonLine SKIP 1 \ The track line number of the horizon \ \ Track lines are one pixel high, and go from 79 (at the \ top of the track view, in the sky), down to 3 (the \ lowest track line, between the mirrors and dashboard) .prevDriverSpeed7 SKIP 1 \ The previous value of trackDriverSpeed when \ calculating the optimum steering for a track segment, \ only used for accessing bit 7 .sectionBehind SKIP 1 \ Used to store the number * 8 of the track section \ behind us when we drive backwards along the track \ \ Track sections are numbered from 0 to 23, so this \ ranges from 0 to 184 .playerSegmentIndex SKIP 1 \ Used to store the index * 3 of the track segment \ containing the player's car in the track segment \ buffer \ \ The player's car is always 32 segments behind the \ front segment in the track segment buffer, so this \ always contains frontSegmentIndex - 96, wrapped around \ as required (as there are three bytes per buffer \ entry) .prevSegmentIndex SKIP 1 \ Used to store the index * 3 of the previous track \ segment .frontSegmentIndex SKIP 1 \ Used to store the index * 3 of the front track \ segment in the track segment buffer \ \ Track segment indexes are numbered from 0 to 39, so \ this ranges from 0 to 117 .directionFacing SKIP 1 \ The direction that our car is facing \ \ * Bit 7 clear = facing forwards \ \ * Bit 7 set = facing backwards .yGravityDelta SKIP 1 \ The distance in the y-axis that the car would fall if \ the track wasn't there, for use when calculating the \ outcome of jumps and being dropped from the crane .vergeType SKIP 1 \ The type of verge that is currently being drawn in \ the DrawTrack routine: \ \ * 0 = leftVergeStart, the left edge of the left \ verge \ \ * 1 = leftTrackStart, the right edge of the left \ verge \ \ * 2 = rightVergeStart, the left edge of the right \ verge \ \ * 3 = rightGrassStart, the right edge of the right \ verge .yJumpHeight SKIP 1 \ The height of the car's jump (or crane lift), in terms \ of track lines, which we use to alter the position of \ the horizon .vergeTopRight SKIP 1 \ The track line just above the segment at \ vergeDepthOfField (i.e. the furthest segment that \ might contain a verge) when drawing the right verge .scaleUp SKIP 1 \ The nominator scale factor for scaling an object \ scaffold (i.e. scale up) \ \ The scaffold is multiplied by scaleUp .scaleDown SKIP 1 \ The denominator scale factor for scaling an object \ scaffold (i.e. scale down) \ \ The scaffold is divided by 2^scaleDown .vergeTopLeft SKIP 1 \ The track line just above the segment at \ vergeDepthOfField (i.e. the furthest segment that \ might contain a verge) when drawing the left verge .heightAboveTrack SKIP 1 \ The car's height above the track, for when it is being \ dropped from the crane, or jumping from hitting the \ verge too fast .playerSpeedLo SKIP 1 \ Low byte of the speed of the player's car along the \ track \ \ This appears to be in mph, with the high byte \ containing the miles per hour, and the low byte \ containing a fraction \ \ Stored as a 16-bit value (playerSpeedHi \ playerSpeedLo) .positionChangeBCD SKIP 1 \ The change in BCD for the player's race position \ \ Gets added to currentPositionBCD when non-zero \ \ Set to the current player's position in BCD in \ ResetVariables .pastHalfway SKIP 1 \ Stores which half of the track the player is in \ \ * 0 = the first half of the track \ \ * 1 = the second half of the track \ \ The halfway point is defined as the middle track \ section, or if there is an odd number of track \ sections, the section before the middle track section \ \ Set to 1 in ResetVariables .currentPositionBCD SKIP 1 \ The current race position in BCD \ \ Displayed at the top of the screen after "Position" .pixelMaskVerge SKIP 1 \ Offset within the vergePixelMask table for segments \ that are close enough to show a verge, i.e. those \ within the verge depth of field .backgroundLeft SKIP 1 \ Details of the verge colour for the left side of the \ verge edge we are drawing, to use as the track line's \ background colour .backgroundRight SKIP 1 \ Details of the verge colour for the right side of the \ verge edge we are drawing, to use as the track line's \ background colour .xPixelCoord SKIP 1 \ The pixel x-coordinate of the centre of the current \ object \ \ In terms of screen coordinates, so 0 is the left edge \ of the screen, 80 is the centre and 159 is the right \ edge .yPixelCoord SKIP 1 \ The pixel y-coordinate of the centre of the current \ object \ \ In terms of track lines, so 80 is the top of the track \ view and 0 is the bottom of the track view .objectType SKIP 1 \ The type of object to draw (0 to 12) \ \ * 0 = Four-object car, front tyres \ * 1 = Four-object car, body and helmet \ * 2 = Four-object car, rear tyres \ * 3 = Four-object car, rear wing \ * 4 = Standard car \ * 5 = Distant car \ * 6 = Corner marker \ * 7 = Straight sign \ * 8 = Start flag \ * 9 = Blank road sign \ * 10 = Chicane road sign \ * 11 = Right turn road sign \ * 12 = Left turn road sign .xPrevVelocityLo SKIP 1 \ Used to store the low byte of the x-coordinate of the \ player's previous velocity vector during the driving \ model calculations (i.e. the velocity from the last \ iteration of the main loop) \ \ Stored as a 16-bit value (xPrevVelocityHi \ xPrevVelocityLo) .xPrevVelocityHi SKIP 1 \ Used to store the high byte of the x-coordinate of the \ player's previous velocity vector during the driving \ model calculations (i.e. the velocity from the last \ iteration of the main loop) \ \ Stored as a 16-bit value (xPrevVelocityHi \ xPrevVelocityLo) .xSpinVelocityLo SKIP 1 \ Used to store the low byte of the scaled spin yaw \ angle during the driving model calculations \ \ Stored as a 16-bit value (xSpinVelocityHi \ xSpinVelocityLo) .xSpinVelocityHi SKIP 1 \ Used to store the high byte of the scaled spin yaw \ angle during the driving model calculations \ \ Stored as a 16-bit value (xSpinVelocityHi \ xSpinVelocityLo) .revCount SKIP 1 \ The current rev count, as shown on the rev counter .engineTorque SKIP 1 \ The power being generated by the engine .throttleBrakeState SKIP 1 \ Denotes whether the throttle or brake are being \ applied \ \ * Bit 7 is set if there is no brake or throttle key \ press \ \ * 0 = brakes are being applied \ \ * 1 = throttle is being applied .throttleBrake SKIP 1 \ The amount of throttle or brake being applied .gearNumber SKIP 1 \ The current gear number \ \ * 0 = reverse \ \ * 1 = neutral \ \ * 2-7 = 1 to 5 .objectDistanceLo SKIP 1 \ The low byte of the distance to the current object \ \ If a car's objectDistanceHi is >= 5, then it is drawn \ as a distant car \ \ Stored as a 16-bit value (objectDistanceHi \ objectDistanceLo) .colourScheme SKIP 0 \ The number of the table colour scheme passed to the \ SetRowColours routine: \ \ Scheme 0: Even rows: 132 on 134 (blue on cyan) \ Odd rows: 134 on 135 (cyan on white) \ \ Scheme 4: Even rows: 129 on 132 (red on blue) \ Odd rows: 131 on 130 (yellow on green) \ \ Scheme 8: Even rows: 131 on 132 (yellow on blue) \ Odd rows: 129 on 135 (red on white) .objectNumber SKIP 0 \ The object number of the four-part car we are drawing .sectionCounter SKIP 0 \ A counter for the track section we are processing .segmentCounter SKIP 0 \ A counter for the track segment we are processing .blockCounter SKIP 0 \ A counter for the dash data block we are processing .secondAxis SKIP 1 \ The number of the second axis to calculate in the \ GetRotationMatrix routine .playerPastSegment SKIP 1 \ Determines whether the player has gone past the \ closest segment to the player (i.e. whether the \ relative yaw angle of the segment is greater than 90 \ degrees) \ \ * Bit 7 clear = not yet \ \ * Bit 7 set = player has gone past the closest \ segment .playerHeading SKIP 1 \ The player's yaw angle, relative to the direction of \ the track, where a heading of 0 means the player is \ pointing straight along the track .driverPrinted SKIP 0 \ The number of the driver we just printed in the \ PrintPositionName routine .thisDriver SKIP 0 \ The number of the car we are currently drawing .xStoreDraw SKIP 0 \ Temporary storage for X so it can be preserved through \ calls to DrawCarInPosition, DrawCarOrSign and \ DrawSegmentEdge .thisYawIndex SKIP 0 \ The index of the yaw angle in the track segment list \ for the verge we are processing .thisSignNumber SKIP 1 \ The sign number that we are currently building .timerAdjust SKIP 1 \ A counter for implementing the clock speed adjustment \ \ Starts out with the value of trackTimerAdjust from the \ track data (see the ProcessTime routine) \ \ Gets decremented on each iteration of the main driving \ loop, looping back to trackTimerAdjust after reaching \ zero \ \ When timerAdjust matches equals trackTimerAdjust, the \ clock timer adds 18/100 of a second rather than the \ usual 9/100 of a second, so the clock timer speeds up \ every time the counter loops round \ \ Decreasing the value of trackTimerAdjust therefore \ speeds up the clock timer, allowing the speed of the \ clock timer to be adjusted on a per-track basis \ \ The Silverstone track has a trackTimerAdjust value of \ 24, so timerAdjust wraps around from 0 to 24 .bottomTrackLine SKIP 1 \ The bottom track line for the current object part .prevEdgeInByte SKIP 1 \ Determines whether we are drawing two edges within the \ same pixel byte in DrawObjectEdge .segmentDirection SKIP 1 \ The relative direction of our car, to use when \ processing segments in the GetSegmentAngles routine \ \ * 0 when our car is facing in the same direction \ \ * 1 when our car is facing in the opposite direction .setSpeedForDriver SKIP 1 \ The driver whose speed will be set on the next call to \ the SetDriverSpeed routine .vergeBufferEnd SKIP 1 \ The index of the last entry in the track verge buffer \ for the side of the track we are currently drawing in \ DrawTrack .prevPitchIndex SKIP 1 \ The index of the pitch angles in the verge buffer for \ the previous entry in the verge buffer when drawing \ the verge edges .positionAhead SKIP 1 \ The number of the position ahead of the current \ player's position \ \ This refers to the current player's position in the \ driversInOrder list \ \ The position ahead of the leader is last place .spinPitchAngle SKIP 1 \ The amount of pitch angle spin that is being applied \ to the player's car .prevYawIndex SKIP 1 \ The index of the yaw angles in the verge buffer for \ the previous entry in the verge buffer when drawing \ the verge edges .vergeDepthOfField SKIP 1 \ The index into the verge buffer of the furthest verge \ mark, so for segments further from the player than \ this point, we do not draw verge marks .horizonListIndex SKIP 1 \ The track section or segment that's on the horizon, \ given in terms of the index within the track section \ list (or the track segment list) \ \ Specifically, entry number horizonListIndex at \ yVergeRight equals horizonLine, as does the same entry \ in yVergeLeft (as the track is level in a left to \ right direction) .prevHorizonIndex SKIP 1 \ The value of horizonListIndex from the previous call \ to the GetTrackAndMarkers routine .updateBackground SKIP 1 \ Flag to indicate we have changed the background colour \ of a track line \ \ Set to non-zero if we draw a track verge in the first \ part of the track line on the left edge of the screen .pixelMaskIndex SKIP 1 \ The index into the vergePixelMask table for the colour \ of the verge edge we are drawing in DrawSegmentEdge .objectDistanceHi SKIP 1 \ The high byte of the distance to the current object \ \ If a car's objectDistanceHi is >= 5, then it is drawn \ as a distant car \ \ Stored as a 16-bit value (objectDistanceHi \ objectDistanceLo) .markerNumber SKIP 1 \ The marker number that we are drawing in the \ DrawCornerMarkers routine .markersToDraw SKIP 1 \ The number of corner markers to draw .gearChangeKey SKIP 1 \ Determines whether or not a gear change key has been \ pressed \ \ * Bit 7 set = a gear change key has been pressed .clutchEngaged SKIP 1 \ Determines whether the clutch is engaged \ \ * Bit 7 clear = clutch is engaged \ \ * Bit 7 set = clutch is not fully engaged .revsOnGearChange SKIP 1 \ The rev count when the gear was last changed .positionBehind SKIP 1 \ The number of the position behind the current player's \ position \ \ This refers to the current player's position in the \ driversInOrder list \ \ The position behind last place is the leader .edgeSegmentPointer SKIP 1 \ The index of the segment within the track verge buffer \ that is closest to the player's car .bumpyGrassHeight SKIP 1 \ The height of the bumps when driving over grass, which \ fluctuates randomly in the range 1 to 7 .edgeYawAngle SKIP 1 \ The yaw angle of the track segment that is closest to \ the player's car, from the point of view of the car .soundRevTarget SKIP 1 \ The target pitch for the revs sound \ \ The pitch for the revs sound moves towards this pitch \ level one step at a time, to simulate the sound of the \ engine "catching up" to the throttle \ \ The target pitch is set to revCount + 25 .soundRevCount SKIP 1 \ The current pitch for the revs sound .engineStatus SKIP 1 \ Whether or not the engine is on \ \ * 0 = engine is off \ \ * &FF = engine is on .resetSectionList SKIP 1 \ Controls whether to reset the contents of the track \ section list \ \ * 0 = do not reset the track section list \ \ * Non-zero = reset the track section list .playerSpeedHi SKIP 1 \ High byte of the speed of the player's car along the \ track \ \ This appears to be in mph, with the high byte \ containing the miles per hour, and the low byte \ containing a fraction \ \ Stored as a 16-bit value (playerSpeedHi \ playerSpeedLo) .printMode SKIP 1 \ Determines how the next character is printed \ on-screen: \ \ * 0 = poke the character directly into screen memory \ \ * 1 = print the character with OSWRCH (for mode 7) .qualifyTimeEnding SKIP 1 \ Determines whether the time warnings have been shown \ at the end of the qualifying time: \ \ * Bit 6 set = the one-minute warning has been shown \ \ * Bit 7 set = the time-up warning has been shown .updateDrivingInfo SKIP 1 \ Determines which parts of the driving information \ should be updated at the top of the screen \ \ * Bit 7 set = update lap number (during a race) \ update lap time (practice/qualifying) \ \ * Bit 6 set = we are driving the first practice or \ qualifying lap, so do not update the \ best lap time \ \ Set to %10000000 in ResetVariables for race laps only .collisionDriver SKIP 1 \ The number of the driver being hit by the player's car .processContact SKIP 1 \ Another car is close enough to the player's car for us \ to process car-on-car contact \ \ * 0 = no car is close enough \ \ * Non-zero = a car is close enough to check for \ contact .lineBufferSize SKIP 1 \ The size of the line buffer \ \ Zeroed in SetupGame .mainLoopCounterLo SKIP 1 \ Low byte of the main loop counter, which increments on \ each iteration of the main driving loop \ \ Stored as a 16-bit value (mainLoopCounterHi \ mainLoopCounterLo) .startingStack SKIP 1 \ The value of the stack pointer when the game starts, \ so we can restore it when restarting the game .raceStarted SKIP 1 \ Flag determining whether the race has started \ \ * Bit 7 clear = this is practice or a qualifying lap \ \ * Bit 7 set = the race has started .raceStarting SKIP 1 \ The current stage of the starting lights at the start \ of the race \ \ When a race is about to start, raceStarting is set to \ 128, and stays on this value until the engine is \ started, at which point it starts to count down, with \ one tick per iteration of the main loop, working \ through the following sequence: \ \ * 128 = show black lights, engine not yet started \ \ * Start counting down from 240 once engine starts \ \ * 240-192 = show black lights \ \ * 191-161 = show blue lights \ \ * 160 = keep showing blue lights and stop counting \ down until main loop counter is a multiple \ of 64 \ \ * Start counting down from 40 once loop counter is \ a multiple of 64 \ \ * 40-1 = show green lights \ \ * 0 = show no lights (race has started) \ \ When bit 7 is set (i.e. raceStarting >= 128), we are \ on the grid, so we do not increment the clock timer in \ ProcessTime, and the MoveCars routine has no effect .numberOfLaps SKIP 1 \ The number of laps in the race (5, 10 or 20) .currentPlayer SKIP 1 \ The number of the current player \ \ * 0 for practice \ \ * 0 to 19 for competition .P SKIP 1 \ Temporary storage, used in a number of places .Q SKIP 1 \ Temporary storage, used in a number of places .R SKIP 1 \ Temporary storage, used in a number of places .S SKIP 1 \ Temporary storage, used in a number of places .T SKIP 1 \ Temporary storage, used in a number of places .U SKIP 1 \ Temporary storage, used in a number of places .edgePixel SKIP 0 \ The current edge, as a one-pixel byte .V SKIP 1 \ Temporary storage, used in a number of places .W SKIP 1 \ Temporary storage, used in a number of places .leftOfEdge SKIP 0 \ The fill colour to the left of the edge we are drawing .G SKIP 1 \ Temporary storage, used in a number of places .rightOfEdge SKIP 0 \ The fill colour to the right of the edge we are \ drawing .H SKIP 1 \ Temporary storage, used in a number of places .I SKIP 1 \ Temporary storage, used in a number of places .J SKIP 1 \ Temporary storage, used in a number of places .prevBlockNumber SKIP 0 \ The dash data block number for the previous edge .K SKIP 1 \ Temporary storage, used in a number of places .L SKIP 1 \ Temporary storage, used in a number of places .thisEdge SKIP 0 \ The current edge that we are drawing as part of an \ object part .M SKIP 1 \ Temporary storage, used in a number of places .topTrackLine SKIP 0 \ The top track line for the current object part .N SKIP 1 \ Temporary storage, used in a number of places .PP SKIP 1 \ Temporary storage, used in a number of places .QQ SKIP 1 \ Temporary storage, used in a number of places .blockOffset SKIP 0 \ The dash data offset for the current edge .RR SKIP 1 \ Temporary storage, used in a number of places .nextEdge SKIP 0 \ The next edge that we are drawing as part of an \ object part (where applicable) .SS SKIP 1 \ Temporary storage, used in a number of places .colourData SKIP 0 \ Colour data for the current object part .TT SKIP 1 \ Temporary storage, used in a number of places .blockNumber SKIP 0 \ The dash data block number for the current edge .UU SKIP 1 \ Temporary storage, used in a number of places .VV SKIP 1 \ Temporary storage, used in a number of places .WW SKIP 1 \ Temporary storage, used in a number of places .GG SKIP 1 \ Temporary storage, used in a number of places .HH SKIP 1 \ This byte does not appear to be used .II SKIP 1 \ Temporary storage, used in a number of places .JJ SKIP 1 \ Temporary storage, used in a number of places .edgePixelMask SKIP 0 \ The pixel mask for the previous edge .pixelMaskNoVerge SKIP 1 \ Offset within the vergePixelMask table for segments \ that are too far away to show a verge, i.e. those \ outside the verge depth of field .nextBlockNumber SKIP 0 \ The dash data block number for the next edge .LL SKIP 1 \ Temporary storage, used in a number of places .MM SKIP 1 \ Temporary storage, used in a number of places .nextEdgeCoord SKIP 0 \ The x-coordinate for the next edge .NN SKIP 1 \ Temporary storage, used in a number of places \ ****************************************************************************** \ \ Name: Stack variables \ Type: Workspace \ Address: &0100 to &0175 \ Category: Workspaces \ Summary: Variables that share page 1 with the stack \ \ ****************************************************************************** ORG &0100 .positionNumber SKIP 0 \ Position numbers to show in the first column of the \ driver table .carStatus SKIP 20 \ Each car's status byte \ \ * Bit 0 = update this carStatus byte when applying \ tactics in the ProcessOvertaking routine \ \ * Clear = do update carStatus \ \ * Set = do not update carStatus \ \ * Bit 4 = affects driving around corners for visible \ cars (see BuildVisibleCar) \ \ * Clear = set carSteering to the segment's \ steering line in segmentSteering when \ going fast enough (carSpeedHi >= 50) \ \ * Set = do not set carSteering in the \ BuildVisibleCar routine (and use the \ value set by ProcessOvertaking instead) \ \ * Bit 6 = acceleration status \ \ * Clear = do not accelerate car \ \ * Set = accelerate car \ \ * Bit 7 = braking status \ \ * Clear = do not apply brakes \ \ * Set = apply brakes .carSteering SKIP 20 \ Contains the steering to apply to each car \ \ * Bits 0-5 = the amount of steering as a positive \ value (0 to 31) \ \ * Bit 6 = controls whether to apply steering in the \ MoveCars routine \ \ * Clear = always apply steering \ \ * Set = only apply steering if there is enough \ room on the track \ \ * Bit 7 = the direction of the steering \ \ * Clear = steer left \ \ * Set = steer right \ \ The steering is stored as a sign-magnitude number, \ where the sign is in bit 7 and the magnitude is in \ bits 0-5 \ \ The amount of steering is stored in terms of the \ change in racing line, where the width of the track is \ 256, so steering by 26 would steer the car sideways by \ 10% of the track width .driverSpeed SKIP 20 \ The average speed of this driver in the race (88 to \ 162) \ \ The speed for each driver depends on a number of \ factors, and is calculated in the SetDriverSpeed \ routine \ \ Indexed by driver number (0 to 19) .driversInOrder SKIP 20 \ A list of driver numbers in order \ \ For example, during a race, this contains the race \ position of each driver in the race (i.e. first place, \ second place etc.) \ \ It is also used to sort drivers by lap time and points \ for the driver table \ \ Indexed by driver number (0 to 19) \ \ Gets set in InitialiseDrivers to the number of each \ driver, so the initial order is driver number .carSpeedHi SKIP 20 \ High byte of each car's forward speed \ \ Stored as an 8-bit value (carSpeedHi carSpeedLo) .carProgress SKIP 20 \ Lowest byte of each car's progress through the segment \ it's in \ \ This is effectively a fractional part of the car's \ progress through the segment, with 0 being the start \ of the segment and 255 the end of the segment \ \ When this byte rolls over, we increment the car's \ segment number in (objectSegmentHi objectSegmentLo) to \ move on to the next segment .carRacingLine SKIP 20 \ Each car's position on the track in the x-axis \ \ This determines how far each car is to the left or \ right on the track - i.e. it's the car's racing line \ \ * 0 is full right \ \ * 128 is the centre line \ \ * 255 is full left \ \ Bit 7 is therefore set if the car is in the left half \ the track, and clear for the right half .objectStatus SKIP 24 \ Various status flags for each object \ \ * Bits 0-3 = the object type \ \ * Bit 6: 0 = the car is still racing \ 1 = the car has finished the race \ \ * Bit 7: 0 = object is visible \ 1 = object is hidden \ \ Set to &80 in ResetVariables .carSectionSpeed SKIP 20 \ Set to the driver speed for the next track section, \ which is taken from the track data and used to set the \ section's maximum speed for non-player drivers \ \ Only applies to sections with bit 7 of the flag byte \ set, in which case carSectionSpeed is set to the \ trackDriverSpeed value from the preceding track \ section \ \ Set to 255 in ResetVariables, which means no minimum \ speed \ ****************************************************************************** \ \ Name: Main variable workspace \ Type: Workspace \ Address: &0380 to &07F8 and &0880 to &0AFF \ Category: Workspaces \ Summary: The main block of game variables \ \ ****************************************************************************** ORG &0380 .objYawAngleLo SKIP 24 \ Low byte of each object's yaw angle \ \ Stored as a 16-bit value (objYawAngleHi objYawAngleLo) .objYawAngleHi SKIP 24 \ High byte of each object's yaw angle \ \ Stored as a 16-bit value (objYawAngleHi objYawAngleLo) .objectPitchAngle SKIP 24 \ Each object's pitch angle .objectSize SKIP 24 \ The size of each of the objects (i.e. the scaled value \ of scaleUp for the object) SKIP 32 \ These bytes appear to be unused .leftSegment SKIP 80 \ For each track line, the index of the segment within \ the track segment list for the left verge .rightSegment SKIP 80 \ For each track line, the index of the segment within \ the track segment list for the right verge .driverGridRow SKIP 20 \ The grid row for each driver (0 to 9) \ \ There are two cars per grid row, with grid row 0 at \ the front including the car in pole position \ \ There are 20 cars, in rows 0 to 9 \ \ Indexed by driver number (0 to 19) \ \ Gets set in InitialiseDrivers .driverLapNumber SKIP 20 \ The current lap number for each driver \ \ Indexed by driver number (0 to 19) .driversInOrder2 SKIP 20 \ Used to store a copy of the driversInOrder list .totalRaceMinutes SKIP 20 \ Minutes of each driver's total race time, stored in \ BCD \ \ Set to &80 in ResetVariables (80 minutes) .totalPointsTop SKIP 20 \ Top byte of total accumulated points for each driver \ \ Indexed by driver number (0 to 19) \ \ Gets set to 0 in InitialiseDrivers \ \ Stored as a 24-bit value (totalPointsTop totalPointsHi \ totalPointsLo) .tyreRightEdge SKIP 80 \ Storage for the first track pixel byte along the right \ edge of the left tyre \ \ This table is used to store the track pixel byte that \ would be shown along the edge of the left tyre, but \ which is partially obscured by the edge \ \ This is stored so we can retrieve it when masking the \ pixel byte with the tyre edge when we draw the track \ line that starts at the edge of the left tyre \ \ There is a byte for each track line from 43 (the track \ line at the top of the dashboard) down to line 3 (the \ lowest track line, just above where the wing mirror \ joins the car body) \ \ Lines 0 to 2 are not used .rightGrassStart SKIP 80 \ For each track line, the block number where the grass \ starts to the right of the track .leftVergeStart SKIP 80 \ For each track line, the block number where the left \ track verge starts .configStop SKIP 1 \ A key has been pressed that stops the race \ \ * Bit 5 set = retire from race/lap \ (SHIFT-f7 pressed) \ \ * Bit 7 and bit 6 set = pit stop \ (SHIFT-f0 pressed) \ \ * Bit 7 set and bit 6 clear = restart game \ (SHIFT and right arrow pressed) \ \ Zeroed in SetupGame .configJoystick SKIP 1 \ A key has been pressed to set joystick or keyboard \ \ * No bits set = keyboard \ (SHIFT-f1 pressed) \ \ * Bit 7 set = joystick \ (SHIFT-f2 pressed) \ \ Zeroed in SetupGame .configVolume SKIP 1 \ A key has been pressed to change the volume \ \ * Bit 7 and bit 6 set = volume down \ (SHIFT-f4 pressed) \ \ * Bit 7 clear and bit 6 set = volume up \ (SHIFT-f5 pressed) \ \ Zeroed in SetupGame .configPause SKIP 1 \ A key has been pressed to pause the game \ \ * Bit 7 set = pause game \ (COPY pressed) \ \ * Bit 6 set = unpause game \ (DELETE pressed) \ \ Zeroed in SetupGame .configAssist SKIP 1 \ A key has been pressed to toggle computer assisted \ steering (CAS) \ \ * No bits set = disable computer assisted steering \ (SHIFT-f3 pressed) \ \ * Bit 7 set = enable computer assisted steering \ (SHIFT-f6 pressed) \ \ Zeroed in SetupGame SKIP 5 \ These bytes appear to be unused .volumeLevel SKIP 1 \ The game's volume level \ \ This uses the operating system's volume scale, with \ -15 being full volume and 0 being silent \ \ Set to -10 (246) in SetupGame SKIP 1 \ This byte appears to be unused .rightVergeStart SKIP 80 \ For each track line, the block number where the right \ track verge starts .leftTrackStart SKIP 80 \ For each track line, the block number where the track \ starts (i.e. the left edge of the black track) .bestLapTenths SKIP 20 \ Tenths of seconds of each driver's best lap time, \ stored in BCD \ \ Indexed by driver number (0 to 19) .clockTenths SKIP 1 \ Tenths of seconds for the clock timer \ \ The clock timer counts the time spent on the track, so \ that's the total amount of qualifying time, or the \ time spent throughout an entire race .lapTenths SKIP 1 \ Tenths of seconds for the lap timer \ \ The lap timer counts the time spent on the current lap SKIP 2 \ These bytes appear to be unused .bestLapSeconds SKIP 20 \ Seconds of each driver's best lap time, stored in BCD \ \ Indexed by driver number (0 to 19) .clockSeconds SKIP 1 \ Seconds for the clock timer \ \ The clock timer counts the time spent on the track, so \ that's the total amount of qualifying time, or the \ time spent throughout an entire race .lapSeconds SKIP 1 \ Seconds for the lap timer \ \ The lap timer counts the time spent on the current lap SKIP 2 \ These bytes appear to be unused .bestLapMinutes SKIP 20 \ Minutes of each driver's best lap time, stored in BCD \ \ Indexed by driver number (0 to 19) .clockMinutes SKIP 1 \ Minutes for the clock timer \ \ The clock timer counts the time spent on the track, so \ that's the total amount of qualifying time, or the \ time spent throughout an entire race .lapMinutes SKIP 1 \ Minutes for the lap timer \ \ The lap timer counts the time spent on the current lap SKIP 2 \ These bytes appear to be unused .objTrackSection SKIP 24 \ The number of the track section * 8 for each object \ \ In the Silverstone track there are 24 track sections \ numbered from 0 to 23, so this ranges from 0 to 184 .segmentVector SKIP 1 \ The segment vector number for a track segment in the \ track segment buffer .segmentSteering SKIP 1 \ The carSteering value to steer round the corner for a \ track segment in the track segment buffer \ \ The various bits are as for carSteering: \ \ * Bits 0-5 = the amount of steering as a positive \ value (0 to 31) \ \ * Bit 6 = controls whether to apply steering in the \ MoveCars routine \ \ * Clear = always apply steering \ \ * Set = only apply steering if there is enough \ room on the track \ \ * Bit 7 = the direction of the steering \ \ * Clear = steer left \ \ * Set = steer right .segmentFlags SKIP 1 \ Flags for a track segment in the track segment buffer \ \ Based on the track section flags for the track section \ containing the segment, but updated for each segment SKIP 39 * 3 \ The track segment buffer contains data for 40 track \ segments, with three bytes per segment, so this \ reserves space for the other 39 SKIP 8 \ These bytes appear to be unused .lineBufferPixel SKIP 40 \ The original screen contents of each pixel in the line \ buffer .lineBufferAddrLo SKIP 40 \ The low byte of the screen address of each pixel in \ the line buffer \ \ Stored as a 16-bit value (lineBufferAddrHi \ lineBufferAddrLo) .lineBufferAddrHi SKIP 40 \ The low byte of the screen address of each pixel in \ the line buffer \ \ Stored as a 16-bit value (lineBufferAddrHi \ lineBufferAddrLo) ORG &0880 .objSectionSegmt SKIP 24 \ Each object's segment number within the current track \ section, counting from the start of the section \ \ This value increments along with objectSegment as the \ object moves through each segment along the track, \ until it reaches the section's trackSectionSize (the \ section's length in terms of segments), at which point \ it resets to zero for the next section \ \ So while objectSegment is the object's segment number \ when looking at the entire track, objSectionSegmt is \ the object's segment number within the current track \ section only .totalRaceTenths SKIP 20 \ Tenths of seconds of each driver's total race time, \ stored in BCD \ \ Indexed by driver number (0 to 19) .totalRaceSeconds SKIP 20 \ Seconds of each driver's total race time, stored in \ BCD \ \ Indexed by driver number (0 to 19) SKIP 16 \ These bytes appear to be unused .objectSegmentLo SKIP 24 \ Low byte of each object's segment, i.e. its position \ around on the track \ \ Segment 0 is on the starting line, and segment numbers \ go from 0 to trackLength as we move along the track, \ before wrapping back to zero again at the end of the \ lap \ \ Set to the value of trackStartLine in ResetVariables \ \ The Silverstone track has a total of 1024 segments \ \ Stored as a 16-bit value (objectSegmentHi \ objectSegmentLo) .objectSegmentHi SKIP 24 \ High byte of each object's segment, i.e. its position \ around on the track \ \ Segment 0 is on the starting line, and segment numbers \ go from 0 to trackLength as we move along the track, \ before wrapping back to zero again at the end of the \ lap \ \ Set to the value of trackStartLine in ResetVariables \ \ The Silverstone track has a total of 1024 segments \ \ Stored as a 16-bit value (objectSegmentHi \ objectSegmentLo) .xSegmentCoordILo SKIP 1 \ The low byte of the 3D x-coordinate for an inner track \ segment in the track segment buffer \ \ Stored as a 16-bit value (xSegmentCoordIHi \ xSegmentCoordILo) .ySegmentCoordILo SKIP 1 \ The low byte of the 3D y-coordinate for an inner track \ segment in the track segment buffer \ \ Stored as a 16-bit value (ySegmentCoordIHi \ ySegmentCoordILo) .zSegmentCoordILo SKIP 1 \ The low byte of the 3D z-coordinate for an inner track \ segment in the track segment buffer \ \ Stored as a 16-bit value (zSegmentCoordIHi \ zSegmentCoordILo) SKIP 39 * 3 \ The track segment buffer contains data for 40 track \ segments, with three bytes per segment, so this \ reserves space for the other 39 .xSegmentCoordOLo SKIP 1 \ The low byte of the 3D x-coordinate for an outer track \ segment in the track segment buffer \ \ Stored as a 16-bit value (xSegmentCoordOHi \ xSegmentCoordOLo) .ySegmentCoordOLo SKIP 1 \ The low byte of the 3D y-coordinate for an outer track \ segment in the track segment buffer \ \ Stored as a 16-bit value (ySegmentCoordOHi \ ySegmentCoordOLo) .zSegmentCoordOLo SKIP 1 \ The low byte of the 3D z-coordinate for an outer track \ segment in the track segment buffer \ \ Stored as a 16-bit value (zSegmentCoordOHi \ zSegmentCoordOLo) SKIP 39 * 3 \ The track segment buffer contains data for 40 track \ segments, with three bytes per segment, so this \ reserves space for the other 39 SKIP 4 \ These bytes appear to be unused .xHelmetCoordLo SKIP 1 \ Low byte of the x-coordinate of the body/helmet object \ in the four-object car \ \ Stored as a 16-bit value (xHelmetCoordHi \ xHelmetCoordLo) .yHelmetCoordLo SKIP 1 \ Low byte of the y-coordinate of the body/helmet object \ in the four-object car \ \ Stored as a 16-bit value (yHelmetCoordHi \ yHelmetCoordLo) .zHelmetCoordLo SKIP 1 \ Low byte of the z-coordinate of the body/helmet object \ in the four-object car \ \ Stored as a 16-bit value (zHelmetCoordHi \ zHelmetCoordLo) SKIP 3 \ These bytes appear to be unused .xCoord1Lo SKIP 1 \ The low byte of the x-coordinate of the temporary \ coordinate variable (xCoord1, yCoord1, zCoord1) \ \ Stored as a 16-bit value (xCoord1Hi xCoord1Lo) .yCoord1Lo SKIP 1 \ The low byte of the y-coordinate of the temporary \ coordinate variable (xCoord1, yCoord1, zCoord1) \ \ Stored as a 16-bit value (yCoord1Hi yCoord1Lo) .zCoord1Lo SKIP 1 \ The low byte of the z-coordinate of the temporary \ coordinate variable (xCoord1, yCoord1, zCoord1) \ \ Stored as a 16-bit value (zCoord1Hi zCoord1Lo) .xCoord2Lo SKIP 1 \ The low byte of the x-coordinate of the temporary \ coordinate variable (xCoord2, yCoord2, zCoord2) \ \ Stored as a 16-bit value (xCoord2Hi xCoord2Lo) .yCoord2Lo SKIP 1 \ The low byte of the y-coordinate of the temporary \ coordinate variable (xCoord2, yCoord2, zCoord2) \ \ Stored as a 16-bit value (yCoord2Hi yCoord2Lo) .zCoord2Lo SKIP 1 \ The low byte of the z-coordinate of the temporary \ coordinate variable (xCoord2, yCoord2, zCoord2) \ \ Stored as a 16-bit value (zCoord2Hi zCoord2Lo) .xSegmentCoordIHi SKIP 1 \ The high byte of the 3D x-coordinate for an inner \ track segment in the track segment buffer \ \ Stored as a 16-bit value (xSegmentCoordIHi \ xSegmentCoordILo) .ySegmentCoordIHi SKIP 1 \ The high byte of the 3D y-coordinate for an inner \ track segment in the track segment buffer \ \ Stored as a 16-bit value (ySegmentCoordIHi \ ySegmentCoordILo) .zSegmentCoordIHi SKIP 1 \ The high byte of the 3D z-coordinate for an inner \ track segment in the track segment buffer \ \ Stored as a 16-bit value (zSegmentCoordIHi \ zSegmentCoordILo) SKIP 39 * 3 \ The track segment buffer contains data for 40 track \ segments, with three bytes per segment, so this \ reserves space for the other 39 .xSegmentCoordOHi SKIP 1 \ The high byte of the 3D x-coordinate for an outer \ track segment in the track segment buffer \ \ Stored as a 16-bit value (xSegmentCoordOHi \ xSegmentCoordOLo) .ySegmentCoordOHi SKIP 1 \ The high byte of the 3D y-coordinate for an outer \ track segment in the track segment buffer \ \ Stored as a 16-bit value (ySegmentCoordOHi \ ySegmentCoordOLo) .zSegmentCoordOHi SKIP 1 \ The high byte of the 3D z-coordinate for an outer \ track segment in the track segment buffer \ \ Stored as a 16-bit value (zSegmentCoordOHi \ zSegmentCoordOLo) SKIP 39 * 3 \ The track segment buffer contains data for 40 track \ segments, with three bytes per segment, so this \ reserves space for the other 39 SKIP 4 \ These bytes appear to be unused .xHelmetCoordHi SKIP 1 \ High byte of the x-coordinate of the body/helmet \ object in the four-object car \ \ Stored as a 16-bit value (xHelmetCoordHi \ xHelmetCoordLo) .yHelmetCoordHi SKIP 1 \ High byte of the y-coordinate of the body/helmet \ object in the four-object car \ \ Stored as a 16-bit value (yHelmetCoordHi \ yHelmetCoordLo) .zHelmetCoordHi SKIP 1 \ High byte of the z-coordinate of the body/helmet \ object in the four-object car \ \ Stored as a 16-bit value (yHelmetCoordHi \ yHelmetCoordLo) SKIP 3 \ These bytes appear to be unused .xCoord1Hi SKIP 1 \ The high byte of the x-coordinate of the temporary \ coordinate variable (xCoord1, yCoord1, zCoord1) \ \ Stored as a 16-bit value (xCoord1Hi xCoord1Lo) .yCoord1Hi SKIP 1 \ The high byte of the y-coordinate of the temporary \ coordinate variable (xCoord1, yCoord1, zCoord1) \ \ Stored as a 16-bit value (yCoord1Hi yCoord1Lo) .zCoord1Hi SKIP 1 \ The high byte of the z-coordinate of the temporary \ coordinate variable (xCoord1, yCoord1, zCoord1) \ \ Stored as a 16-bit value (zCoord1Hi zCoord1Lo) .xCoord2Hi SKIP 1 \ The high byte of the x-coordinate of the temporary \ coordinate variable (xCoord2, yCoord2, zCoord2) \ \ Stored as a 16-bit value (xCoord2Hi xCoord2Lo) .yCoord2Hi SKIP 1 \ The high byte of the y-coordinate of the temporary \ coordinate variable (xCoord2, yCoord2, zCoord2) \ \ Stored as a 16-bit value (yCoord2Hi yCoord2Lo) .zCoord2Hi SKIP 1 \ The high byte of the z-coordinate of the temporary \ coordinate variable (xCoord2, yCoord2, zCoord2) \ \ Stored as a 16-bit value (zCoord2Hi zCoord2Lo) \ ****************************************************************************** \ \ REVS MAIN GAME CODE \ \ Produces the binary file Revs.bin that contains the main game code. \ \ ****************************************************************************** \ ****************************************************************************** \ \ Name: Entry \ Type: Subroutine \ Category: Setup \ Summary: The main entry point for the game: move code into upper memory and \ call it \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** ORG &1200 .Entry LDY #0 \ We start by copying the following block in memory: \ \ * &1200-&12FF is copied to &7900-&79FF \ \ so set up a byte counter in Y .entr1 LDA &1200,Y \ Copy the Y-th byte of &1200 to the Y-th byte of &7900 STA &7900,Y INY \ Increment the byte counter BNE entr1 \ Loop back until we have copied a whole page of bytes JMP SwapCode \ Jump to the routine that we just moved to continue the \ setup process COPYBLOCK &1200, &120E, &7900 \ This code starts out at &1200 and is run \ there, before it duplicates itself to \ &7900-&790D along with the rest of the \ page, so we need to move this code next to \ the block that runs at &790E-&79FF CLEAR &1200, &120E \ We also need to clear the code from &1200 so we can \ assemble more code at the same location \ ****************************************************************************** \ \ Name: SwapCode \ Type: Subroutine \ Category: Setup \ Summary: Move the track data to the right place and run a checksum on it \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** ORG &790E .SwapCode LDA #200 \ Call OSBYTE with A = 200, X = 3 and Y = 0 to disable LDX #3 \ the ESCAPE key and clear memory if the BREAK key is LDY #0 \ pressed JSR OSBYTE LDA #140 \ Call OSBYTE with A = 140 and X = 0 to select the tape LDX #0 \ filing system (i.e. do a *TAPE command) JSR OSBYTE \ We now want to move the track data from trackLoad \ (which is where the loading process loads the track \ file) to trackData (which is where the game expects \ to find the track data) \ \ At the same time, we also want to move the data that \ is currently at trackData, which is part of the \ dashboard image, into screen memory at trackLoad \ \ trackLoad is &70DB and trackData is &5300, so we \ want to do the following: \ \ * Swap &70DB-&77FF and &5300-&5A24 \ \ At the same time, we want to perform a checksum on the \ track data and compare the results with the four \ checksum bytes in trackChecksum \ \ The following does this in batches of 256 bytes, using \ Y as an index that goes from 0 to 255. The checks are \ done at the end of the loop, and they check the value \ of Y first (against &25), and then the high byte of \ the higher address (against &77) but only if the Y \ test fails, so the swaps end up being: \ \ * Swap &5300 + 0-255 with &70DB + 0-255 \ * Swap &5400 + 0-255 with &71DB + 0-255 \ * Swap &5500 + 0-255 with &72DB + 0-255 \ * Swap &5600 + 0-255 with &73DB + 0-255 \ * Swap &5700 + 0-255 with &74DB + 0-255 \ * Swap &5800 + 0-255 with &75DB + 0-255 \ * Swap &5900 + 0-255 with &76DB + 0-255 \ * Swap &5A00 + 0-&24 with &77DB + 0-&24 \ \ So the last operation swaps &5A24 and &77FF LDA #LO(trackData) \ Set (Q P) = trackData STA P \ LDA #HI(trackData) \ so that's one address for the swap STA Q LDA #LO(trackLoad) \ Set (S R) = trackLoad STA R \ LDA #HI(trackLoad) \ so that's the other address for the swap STA S LDY #0 \ Set a byte counter in Y for the swap .swap1 LDA (R),Y \ Swap the Y-th bytes of (Q P) and (S R) PHA LDA (P),Y STA (R),Y PLA STA (P),Y AND #3 \ Decrement the relevant checksum byte TAX \ DEC trackChecksum,X \ The checksum bytes work like this: \ \ * trackChecksum+0 counts the number of data bytes \ ending in %00 \ \ * trackChecksum+1 counts the number of data bytes \ ending in %01 \ \ * trackChecksum+2 counts the number of data bytes \ ending in %10 \ \ * trackChecksum+3 counts the number of data bytes \ ending in %11 \ \ This code checks off the relevant checksum byte for \ the data byte in A, so if all the data is correct, \ this will eventually decrement all four bytes to zero INY \ Increment the loop counter BNE swap2 \ If we have finished swapping a whole page of bytes, INC Q \ increment the high bytes of (Q P) and (S R) to move INC S \ on to the next page .swap2 CPY #&25 \ If we have not yet reached addresses &5A24 and &77FF, BNE swap1 \ jump back to swap1 to keep swapping data LDA S CMP #&77 BNE swap1 LDX #3 \ The data swap is now done, so we now check that all \ three checksum bytes at trackChecksum are zero, so set \ a counter in X to work through the four bytes .swap3 LDA trackChecksum,X \ If the X-th checksum byte is non-zero, the checksum BNE swap4 \ has failed, so jump to swap4 to reset the machine DEX \ Decrement the checksum byte counter BPL swap3 \ Loop back to check the next checksum byte BMI MoveCode \ If we get here then all four checksum bytes are zero, \ so jump to swap4 to keep going (this BMI is \ effectively a JMP as we just passed through a BPL) .swap4 JMP (&FFFC) \ The checksum has failed, so reset the machine \ ****************************************************************************** \ \ Name: MoveCode \ Type: Subroutine \ Category: Setup \ Summary: Move and reset various blocks around in memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .MoveCode \ We are going to process the five memory blocks defined \ in (blockStartHi blockStartLo)-(blockEndHi blockEndLo) \ \ We will either zero the memory block (for the first \ block in the table), or move the block to the address \ in (blockToHi blockToLo) \ \ We work through the blocks from the last entry to the \ first, so we end up doing this: \ \ * Move &1500-&15DA to &7000-&70DA \ * Move &1300-&14FF to &0B00-&0CFF \ * Move &5A80-&645B to &0D00-&16DB \ * Move &64D0-&6BFF to &5FD0-&63FF \ * Zero &5A80-&5E3F LDX #4 \ Set a block counter in X to work through the five \ memory blocks, starting with the block defined at \ the end of the block tables LDY #0 \ Set Y as a byte counter .move1 LDA blockStartLo,X \ Set (Q P) to the X-th address from (blockStartHi STA P \ blockStartLo) LDA blockStartHi,X STA Q LDA blockToLo,X \ Set (S R) to the X-th address from (blockToHi STA R \ blockToLo) LDA blockToHi,X STA S .move2 LDA (P),Y \ Copy the Y-th byte of (Q P) to the Y-th byte of (S R) STA (R),Y \ \ The LDA (P),Y instruction gets modified to LDA #0 for \ the last block that we process, i.e. when X = 0 INC P \ Increment the address in (Q P), starting with the low \ byte BNE move3 \ Increment the high byte if we cross a page boundary INC Q .move3 INC R \ Increment the address in (S R), starting with the low \ byte BNE move4 \ Increment the high byte if we cross a page boundary INC S .move4 LDA P \ If (Q P) <> (blockEndHi blockEndLo) then jump back to CMP blockEndLo,X \ move2 to process the next byte in the block BNE move2 LDA Q CMP blockEndHi,X BNE move2 DEX \ We have finished processing a block, so decrement the \ block counter in X to move on to the next block (i.e. \ the previous entry in the table) BMI move5 \ If X < 0 then we have finished processing all five \ blocks, so jump to move5 BNE move1 \ If X <> 0, i.e. X > 0, then jump up to move1 to move \ the next block LDA ldaZero \ We get here when X = 0, which means we have reached STA move2 \ the last block to process (i.e. the first entry in the LDA ldaZero+1 \ block tables) STA move2+1 \ \ We don't want to copy this block, we want to zero it, \ so we modify the instruction at move2 to LDA #0, so \ the code zeroes the block rather than moving it JMP move1 \ Jump back to move1 to zero the final block .move5 IF _ACORNSOFT OR _4TRACKS JMP SetupGame \ If we get here we have processed all the blocks in the \ block tables, so jump to SetupGame to continue setting \ up the game ELIF _SUPERIOR OR _REVSPLUS JMP Protect \ If we get here we have processed all the blocks in the \ block tables, so jump to Protect to continue setting \ up the game ENDIF \ ****************************************************************************** \ \ Name: ldaZero \ Type: Variable \ Category: Setup \ Summary: Contains code that's used for modifying the MoveCode routine \ \ ****************************************************************************** .ldaZero LDA #0 \ The instruction at move2 in the MoveCode routine is \ modified to this instruction so the routine zeroes a \ block of memory rather than moving it \ ****************************************************************************** \ \ Name: blockStartLo \ Type: Variable \ Category: Setup \ Summary: Low byte of the start address of blocks moved by the MoveCode \ routine \ \ ****************************************************************************** .blockStartLo EQUB &80, &D0 EQUB &80, &00 EQUB &00 \ ****************************************************************************** \ \ Name: blockStartHi \ Type: Variable \ Category: Setup \ Summary: High byte of the start address of blocks moved by the MoveCode \ routine \ \ ****************************************************************************** .blockStartHi EQUB &5A, &64 EQUB &5A, &13 EQUB &15 \ ****************************************************************************** \ \ Name: blockEndLo \ Type: Variable \ Category: Setup \ Summary: Low byte of the end address of blocks moved by the MoveCode \ routine \ \ ****************************************************************************** .blockEndLo EQUB &40, &00 EQUB &5C, &00 EQUB &DB \ ****************************************************************************** \ \ Name: blockEndHi \ Type: Variable \ Category: Setup \ Summary: High byte of the end address of blocks moved by the MoveCode \ routine \ \ ****************************************************************************** .blockEndHi EQUB &5E, &6C EQUB &64, &15 EQUB &15 \ ****************************************************************************** \ \ Name: blockToLo \ Type: Variable \ Category: Setup \ Summary: Low byte of the destination address of blocks moved by the \ MoveCode routine \ \ ****************************************************************************** .blockToLo EQUB &80, &D0 EQUB &00, &00 EQUB &00 \ ****************************************************************************** \ \ Name: blockToHi \ Type: Variable \ Category: Setup \ Summary: High byte of the destination address of blocks moved by the \ MoveCode routine \ \ ****************************************************************************** .blockToHi EQUB &5A, &5F EQUB &0D, &0B EQUB &70 EQUB &09, &B9 \ These bytes appear to be unused EQUB &02, &50 EQUB &9D, &01 EQUB &09, &9D EQUB &79, &09 EQUB &B9, &03 EQUB &50, &9D EQUB &02, &09 EQUB &B9, &01 EQUB &51, &9D EQUB &00, &0A EQUB &B9, &02 EQUB &51, &9D EQUB &01, &0A EQUB &9D, &79 EQUB &0A, &B9 EQUB &03, &51 EQUB &9D, &02 EQUB &0A, &B9 EQUB &04, &50 EQUB &9D, &78 EQUB &09, &B9 EQUB &06, &50 EQUB &9D, &7A EQUB &09, &B9 EQUB &04 \ ****************************************************************************** \ \ Name: soundData \ Type: Variable \ Category: Sound \ Summary: OSWORD blocks for making the various game sounds \ Deep dive: The engine sounds \ \ ------------------------------------------------------------------------------ \ \ Sound data. To make a sound, the MakeSound passes the bytes in this table to \ OSWORD 7. These bytes are the OSWORD equivalents of the parameters passed to \ the SOUND keyword in BASIC. The parameters have these meanings: \ \ channel/flush, amplitude (or envelope number if 1-4), pitch, duration \ \ where each value consists of two bytes, with the low byte first and the high \ byte second. \ \ For the channel/flush parameter, the top nibble of the low byte is the flush \ control (where a flush control of 0 queues the sound, and a flush control of \ 1 makes the sound instantly), while the bottom nibble of the low byte is the \ channel number. When written in hexadecimal, the first figure gives the flush \ control, while the second is the channel (so &13 indicates flush control = 1 \ and channel = 3). \ \ ****************************************************************************** ORG &0B00 EQUB &10, &10 \ These bytes appear to be unused EQUB &10, &10 EQUB &10, &10 EQUB &10, &10 EQUB &10, &10 EQUB &10, &10 EQUB &10, &10 EQUB &10, &10 .soundData EQUB &10, &00 \ Sound #0: Engine exhaust (SOUND &10, -10, 3, 255) EQUB &F6, &FF EQUB &03, &00 EQUB &FF, &00 EQUB &11, &00 \ Sound #1: Engine tone 1 (SOUND &11, -10, 187, 255) EQUB &F6, &FF EQUB &BB, &00 EQUB &FF, &00 EQUB &12, &00 \ Sound #2: Engine tone 2 (SOUND &12, -10, 40, 255) EQUB &F6, &FF EQUB &28, &00 EQUB &FF, &00 EQUB &13, &00 \ Sound #3: Tyre squeal (SOUND &13, 1, 130, 255) EQUB &01, &00 EQUB &82, &00 EQUB &FF, &00 EQUB &10, &00 \ Sound #4: Crash/contact (SOUND &10, -10, 6, 4) EQUB &F6, &FF EQUB &06, &00 EQUB &04, &00 \ ****************************************************************************** \ \ Name: envelopeData \ Type: Variable \ Category: Sound \ Summary: Data for the sound envelope for squealing tyres \ Deep dive: The engine sounds \ \ ------------------------------------------------------------------------------ \ \ There is only one sound envelope defined in Revs: \ \ * Envelope 1 defines the sound of the tyres squealing \ \ ****************************************************************************** .envelopeData EQUB 1, 1, 2, -2, -6, 4, 1, 1, 10, 0, 0, 0, 72, 0 \ ****************************************************************************** \ \ Name: xStoreSound \ Type: Variable \ Category: Sound \ Summary: Temporary storage for X so it can be preserved through calls to \ the sound routines \ \ ****************************************************************************** .xStoreSound EQUB &FF \ ****************************************************************************** \ \ Name: MakeSound \ Type: Subroutine \ Category: Sound \ Summary: Make a sound \ Deep dive: The engine sounds \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ A The sound number from the soundData table (0 to 4) \ \ Y The volume level to use for the sound, or the envelope \ number (the latter is used for sound #3 only, and is \ always set to envelope 1, which is the only envelope) \ \ ------------------------------------------------------------------------------ \ \ Other entry points: \ \ MakeSound-3 Make the sound at the current volume level \ \ ****************************************************************************** LDY volumeLevel \ Set Y to the current volumeLevel, to use as the sound \ amplitude below .MakeSound STX xStoreSound \ Store the value of X in xStoreSound, so we can \ preserve it through the call to the MakeSound routine ASL A \ Set A = A * 8 ASL A \ ASL A \ so we can use it as an index into the soundData table, \ which has 8 bytes per entry CLC \ Set (Y X) = soundData + A ADC #LO(soundData) \ TAX \ starting with the low byte in X, which gets set to the \ following, as LO(soundData) is 16: \ \ * 16 for sound #0 \ * 24 for sound #1 \ * 32 for sound #2 \ * 40 for sound #3 \ * 48 for sound #4 \ \ This means that soundData - 16 + X points to the sound \ data block for the sound we are making, which we now \ use to set the volume or envelope for the sound to Y, \ and flag the correct sound buffer as being in use TYA \ Set byte #2 of the sound data (low byte of amplitude STA soundData-16+2,X \ or envelope number) to Y LDA soundData-16,X \ Set Y to byte #0 of the sound data (channel/flush), AND #3 \ and extract the channel number into Y TAY LDA #7 \ Set A = 7 for the OSWORD command to make a sound STA soundBuffer,Y \ Set the Y-th sound buffer status to 7, which is \ non-zero and indicates that we are making a sound on \ this channel BNE MakeSoundEnvelope \ Jump to MakeSoundEnvelope to set up Y and apply the \ OSWORD command to the (Y X) block, which makes the \ relevant sound (this BNE is effectively a JMP as A is \ never zero) \ ****************************************************************************** \ \ Name: DefineEnvelope \ Type: Subroutine \ Category: Sound \ Summary: Define a sound envelope \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ A The offset of the sound envelope data in envelopeData: \ \ * A = 0 for the first (and only) envelope definition \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ X X is unchanged \ \ ****************************************************************************** .DefineEnvelope STX xStoreSound \ Store the value of X in xStoreSound, so we can \ preserve it through the call to the DefineEnvelope \ routine CLC \ Set (Y X) = envelopeData + A ADC #LO(envelopeData) \ TAX \ starting with the low byte LDA #8 \ Set A = 8 for the OSWORD command to define an envelope \ Fall through into MakeSoundEnvelope to set up Y and \ apply the OSWORD command to the (Y X) block, which \ defines the relevant sound envelope \ ****************************************************************************** \ \ Name: MakeSoundEnvelope \ Type: Subroutine \ Category: Sound \ Summary: Either make a sound or set up an envelope \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ A The action: \ \ * A = 7 make a sound \ \ * A = 8 to define a sound envelope \ \ X The low byte of the address of the OSWORD block \ \ xStoreSound The value of X to restore at the end of the routine \ \ ****************************************************************************** .MakeSoundEnvelope LDY #HI(soundData) \ Set y to the high byte of the soundData block \ address, so (Y X) now points to the relevant envelope \ or sound data block JSR OSWORD \ Call OSWORD with action A, as follows: \ \ * A = 7 to make the sound at (Y X) \ \ * A = 8 to set up the sound envelope at (Y X) LDX xStoreSound \ Restore the value of X we stored before calling the \ routine, so it doesn't change RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: ScaleWingSettings \ Type: Subroutine \ Category: Driving model \ Summary: Scale the wing settings and calculate the wing balance, for use in \ the driving model \ \ ------------------------------------------------------------------------------ \ \ The wing settings (0 to 40) are scaled to the range 90 to 218. \ \ The wing balance is calculated as: \ \ 60 + (rearWingSetting * 3 + frontWingSetting) / 2 \ \ which is in the range 60 to 140, with higher numbers when the rear wing is \ greater (i.e. pushes down more) than the front wing. \ \ ****************************************************************************** .ScaleWingSettings LDX #1 \ We are about to loop through the two wings, so set a \ counter in X so we do the rear wing setting first, and \ then the front wing setting .wing1 LDA frontWingSetting,X \ Set U = wing setting * 4 ASL A \ ASL A \ where the wing setting was entered by the player STA U LDA wingScaleFactor,X \ Set A to the wingScaleFactor for this wing, which is \ hard-coded to 205 for both wings JSR Multiply8x8 \ Set (A T) = A * U \ So by this point, we have: \ \ A = A * U / 256 \ = U * 205 / 256 \ = wing setting * 4 * 205 / 256 \ = wing setting * 820 / 256 \ \ The wing settings can be from 0 to 40, so this scales \ the setting to the range 0 to 128 CLC \ Set A = A + 90 ADC #90 \ \ which is in the range 90 to 218 STA wingSetting,X \ Store the scaled wing setting in wingSetting DEX \ Decrement the loop counter BPL wing1 \ Loop back until we have scaled both wing settings LDA rearWingSetting \ Set A = (rearWingSetting * 2 + rearWingSetting ASL A \ + frontWingSetting) / 2 + 60 ADC rearWingSetting \ = (rearWingSetting * 3 + frontWingSetting) / 2 ADC frontWingSetting \ + 60 LSR A \ ADC #60 \ which is in the range 60 to 140, with higher numbers \ when the rear wing is greater than the front wing STA wingBalance \ Store the wing balance in wingBalance RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: wingScaleFactor \ Type: Variable \ Category: Driving model \ Summary: Scale factors for the wing settings \ \ ****************************************************************************** .wingScaleFactor EQUB 205 \ Scale factor for the front wing setting EQUB 205 \ Scale factor for the rear wing setting \ ****************************************************************************** \ \ Name: SpinTrackSection \ Type: Subroutine \ Category: Track geometry \ Summary: Apply spin to a section in the track section list \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ Y The index of the entry in the track section list: \ \ Y = 0 to 5 (update the right verge): \ \ * Reset vergeDataRight to zero \ \ * Subtract spinYawAngle from the yaw angles in \ xVergeRightLo, xVergeRightHi \ \ * Subtract spinPitchAngle from the pitch angle in \ yVergeRight \ \ * Update horizonListIndex and horizonLine \ \ Y = 0 to 5 + 40 (update the left verge): \ \ * Reset vergeDataLeft to zero \ \ * Subtract spinYawAngle from the yaw angles in \ xVergeLeftLo, xVergeLeftHi \ \ * Subtract spinPitchAngle from the pitch angle in \ yVergeLeft \ \ * Update horizonListIndex and horizonLine \ \ ****************************************************************************** .SpinTrackSection LDA #0 \ Set the Y-th entry in vergeDataRight to 0, to reset STA vergeDataRight,Y \ the colour of the verge mark to black (this is \ recalculated in the GetVergeAndMarkers routine) LDA xVergeRightLo,Y \ Set xVergeRight = xVergeRight - spinYawAngle SEC \ SBC spinYawAngleHi \ starting with the low bytes STA xVergeRightLo,Y LDA xVergeRightHi,Y \ And then the high bytes SBC spinYawAngleTop STA xVergeRightHi,Y LDA yVergeRight,Y \ Set A = Y-th entry in yVergeRight - spinPitchAngle SEC SBC spinPitchAngle STA yVergeRight,Y \ Store the result in the Y-th entry in yVergeRight CMP horizonLine \ If A < horizonLine, then this track section is lower BCC rott1 \ than the current horizon, so jump to rott1 to return \ from the subroutine STA horizonLine \ Otherwise this track section is higher than the \ current horizon pitch angle, so the track obscures the \ horizon and we need to update horizonLine to this \ new pitch angle STY horizonListIndex \ Set horizonListIndex to the track section list index \ number in Y .rott1 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: AddVectors \ Type: Subroutine \ Category: Maths (Geometry) \ Summary: Add two three-axis vectors together \ \ ------------------------------------------------------------------------------ \ \ Given a three-axis variable vectorX and a three-axis variable vectorY, this \ routine calculates the following addition: \ \ [ (SS T) ] \ vectorX = vectorY + [ (TT U) ] \ [ (UU V) ] \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The offset of the vectorX variable to update: \ \ * Index * 3 of the track segment to use for the \ section coordinates for the inner track \ \ * &F4 = xHelmetCoord \ \ * &FA = xCoord1 \ \ * &FD = xCoord2 \ \ Y The offset of the vectorY variable to add: \ \ * Index * 3 of the track segment to use for the \ section coordinates for the inner track \ \ * &F4 = xHelmetCoord \ \ * &FA = xCoord1 \ \ * &FD = xCoord2 \ \ (SS T) The value to add to the first axis \ \ (TT U) The value to add to the second axis \ \ (UU V) The value to add to the third axis \ \ ****************************************************************************** .AddVectors LDA xSegmentCoordILo,Y \ Set xVectorX = xVectorY + (SS T) CLC \ ADC T \ starting with the low bytes STA xSegmentCoordILo,X LDA xSegmentCoordIHi,Y \ And then the high bytes ADC SS STA xSegmentCoordIHi,X LDA ySegmentCoordILo,Y \ Set yVectorX = yVectorY + (TT U) CLC \ ADC U \ starting with the low bytes STA ySegmentCoordILo,X LDA ySegmentCoordIHi,Y \ And then the high bytes ADC TT STA ySegmentCoordIHi,X LDA zSegmentCoordILo,Y \ Set zVectorX = zVectorY + (UU V) CLC \ ADC V \ starting with the low bytes STA zSegmentCoordILo,X LDA zSegmentCoordIHi,Y \ And then the high bytes ADC UU STA zSegmentCoordIHi,X RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: Multiply8x8 \ Type: Subroutine \ Category: Maths (Arithmetic) \ Summary: Calculate (A T) = T * U \ \ ------------------------------------------------------------------------------ \ \ Do the following multiplication of two unsigned 8-bit numbers: \ \ (A T) = A * U \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ X X is unchanged \ \ ------------------------------------------------------------------------------ \ \ Other entry points: \ \ Multiply8x8+2 Calculate (A T) = T * U \ \ ****************************************************************************** .Multiply8x8 STA T \ Set T = A \ We now calculate (A T) = T * U \ = A * U LDA #0 \ Set A = 0 so we can start building the answer in A LSR T \ Set T = T >> 1 \ and C flag = bit 0 of T \ We are now going to work our way through the bits of \ T, and do a shift-add for any bits that are set, \ keeping the running total in A, and instead of using a \ loop, we unroll the calculation, starting with bit 0 BCC P%+5 \ If C (i.e. the next bit from T) is set, do the CLC \ addition for this bit of T: ADC U \ \ A = A + U ROR A \ Shift A right to catch the next digit of our result, \ which the next ROR sticks into the left end of T while \ also extracting the next bit of T ROR T \ Add the overspill from shifting A to the right onto \ the start of T, and shift T right to fetch the next \ bit for the calculation into the C flag BCC P%+5 \ Repeat the shift-and-add loop for bit 1 CLC ADC U ROR A ROR T BCC P%+5 \ Repeat the shift-and-add loop for bit 2 CLC ADC U ROR A ROR T BCC P%+5 \ Repeat the shift-and-add loop for bit 3 CLC ADC U ROR A ROR T BCC P%+5 \ Repeat the shift-and-add loop for bit 4 CLC ADC U ROR A ROR T BCC P%+5 \ Repeat the shift-and-add loop for bit 5 CLC ADC U ROR A ROR T BCC P%+5 \ Repeat the shift-and-add loop for bit 6 CLC ADC U ROR A ROR T BCC P%+5 \ Repeat the shift-and-add loop for bit 7 CLC ADC U ROR A ROR T RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: Divide8x8 \ Type: Subroutine \ Category: Maths (Arithmetic) \ Summary: Calculate T = 256 * A / V \ \ ------------------------------------------------------------------------------ \ \ In the same way that shift-and-add implements a binary version of the manual \ long multiplication process, shift-and-subtract implements long division. We \ shift bits out of the left end of the number being divided (A), subtracting \ the largest possible multiple of the divisor (V) after each shift; each bit of \ A where we can subtract Q gives a 1 the answer to the division, otherwise it \ gives a 0. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ T This has something to do with rounding the result \ \ A Unsigned integer \ \ V Unsigned integer \ \ ****************************************************************************** .Divide8x8 ASL T \ Shift T left, which clears bit 0 of T, ready for us to \ start building the result \ We now repeat the following five instruction block \ eight times, one for each bit in T ROL A \ Shift A to the left to extract the next bit from the \ number being divided BCS P%+6 \ If we just shifted a 1 out of A, skip the next two \ instructions and jump straight to the subtraction CMP V \ If A < V skip the following two instructions with the BCC P%+5 \ C flag clear, so we shift a 0 into the result in T SBC V \ A >= V, so set A = A - V and set the C flag so we SEC \ shift a 1 into the result in T ROL T \ Shift T to the left, pulling the C flag into bit 0 ROL A \ Repeat the shift-and-subtract loop for bit 1 BCS P%+6 CMP V BCC P%+5 SBC V SEC ROL T ROL A \ Repeat the shift-and-subtract loop for bit 2 BCS P%+6 CMP V BCC P%+5 SBC V SEC ROL T ROL A \ Repeat the shift-and-subtract loop for bit 3 BCS P%+6 CMP V BCC P%+5 SBC V SEC ROL T ROL A \ Repeat the shift-and-subtract loop for bit 4 BCS P%+6 CMP V BCC P%+5 SBC V SEC ROL T ROL A \ Repeat the shift-and-subtract loop for bit 5 BCS P%+6 CMP V BCC P%+5 SBC V SEC ROL T ROL A \ Repeat the shift-and-subtract loop for bit 6 BCS P%+6 CMP V BCC P%+5 SBC V SEC ROL T ROL A \ Repeat the shift-and-subtract loop for bit 7, but BCS P%+4 \ without the subtraction, as we don't need to keep CMP V \ calculating A once its top bit has been extracted ROL T RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: GetObjectDistance \ Type: Subroutine \ Category: 3D objects \ Summary: Calculate the distance between an object and the player's car, for \ collision purposes \ \ ------------------------------------------------------------------------------ \ \ This routine is called with the smaller yaw angle of the object, where 0 to \ 255 represents 0 to 45 degrees, so 103 = 18.2 degrees. The smaller viewing \ angle is taken from the arctan calculation for the yaw angle calculation in \ GetObjYawAngle, so that's the triangle whose hypotenuse is the line between \ the player and the object, and whose other sides are parallel to the x-axis \ and z-axis. \ \ If the smaller yaw angle is < 18.2 degrees, the routine does this: \ \ * Set (L K) = (J I) + (H G) / 8 \ = max + min / 8 \ \ If the smaller yaw angle is >= 18.2 degrees, the routine does this: \ \ * Set (L K) = (J I) * 7/8 + (H G) / 2 \ = max * 7/8 + min / 2 \ \ This appears to set the distance between the object and the player's car, for \ the purposes of determining whether contact has been made. \ \ I suspect the calculation is an approximation of Pythagoras that is much \ faster to calculate, split into small yaw angles (when the objects are \ close to being orthogonal to each other) and larger yaw angles (when their \ relative positions are closer to the diagonal). This is a guess, though. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ (J I) max(|x-delta|, |z-delta|) \ \ (H G) min(|x-delta|, |z-delta|) \ \ M The smaller yaw angle of the object, where 0 to 255 \ represents 0 to 45 degrees, so 103 = 18.2 degrees \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ (L K) The distance between the object and the player's car \ \ A Contains the high byte of (L K) \ \ ****************************************************************************** .GetObjectDistance LDA M \ If M >= 103, jump to odis1 CMP #103 BCS odis1 LDA G \ Set A = G LSR H \ Set (H A) = (H A) >> 3 ROR A \ = (H G) >> 3 LSR H ROR A LSR H ROR A CLC \ Set (L K) = (J I) + (H A) ADC I \ = (J I) + (H G) >> 3 STA K \ = (J I) + (H G) / 8 LDA H ADC J STA L RTS \ Return from the subroutine .odis1 LSR H \ Set (H G) = (H G) >> 1 ROR G LDA J \ Set (T A) = (J I) STA T LDA I LSR T \ Set (T U) = (T A) >> 3 ROR A \ = (J I) >> 3 LSR T ROR A LSR T ROR A STA U LDA G \ Set (L K) = (J I) + (H G) CLC \ = (J I) + (H G) >> 1 ADC I STA K LDA H ADC J STA L LDA K \ Set (L K) = (L K) - (T U) SEC \ = (J I) + (H G) >> 1 - (J I) >> 3 SBC U \ = (J I) * 7/8 + (H G) / 2 STA K LDA L SBC T STA L RTS \ Return from the subroutine EQUB &F1, &0C \ These bytes appear to be unused EQUB &E5, &74 EQUB &8D, &F6 EQUB &0C, &60 EQUB &00, &00 EQUB &00, &00 EQUB &00, &00 EQUB &40 \ ****************************************************************************** \ \ Name: GetRotationMatrix (Part 1 of 5) \ Type: Subroutine \ Category: Maths (Geometry) \ Summary: Calculate the rotation matrix for rotating the player's yaw angle \ into the global 3D coordinate system \ Deep dive: The core driving model \ Trigonometry \ \ ------------------------------------------------------------------------------ \ \ This routine calculates the following: \ \ sinYawAngle = sin(playerYawAngle) \ cosYawAngle = cos(playerYawAngle) \ \ We can use these to create a rotation matrix that rotates the yaw angle from \ the player's frame of reference into the global 3D coordinate system. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ (A X) Player yaw angle in (playerYawAngleHi playerYawAngleLo) \ \ ****************************************************************************** .GetRotationMatrix STA J \ Set (J T) = (A X) STX T \ = playerYawAngle JSR GetAngleInRadians \ Set (U A) to the playerYawAngle, reduced to a quarter \ circle, converted to radians, and halved \ \ Let's call this yawRadians / 2, where yawRadians is \ the reduced player yaw angle in radians STA G \ Set (U G) = (U A) = yawRadians / 2 LDA U \ Set (A G) = (U G) = yawRadians / 2 STA H \ Set (H G) = (A G) = yawRadians / 2 \ So we now have: \ \ (H G) = (A G) = (U G) = yawRadians / 2 \ \ This is the angle vector that we now project onto the \ x- and z-axes of the world 3D coordinate system LDX #1 \ Set X = 0 and secondAxis = 1, so we project sin(H G) STX secondAxis \ into sinYawAngle and cos(H G) into cosYawAngle LDX #0 BIT J \ If bit 6 of J is clear, then playerYawAngle is in one BVC rotm1 \ of these ranges: \ \ * 0 to 63 (%00000000 to %00111111) \ \ * -128 to -65 (%10000000 to %10111111) \ \ The degree system in Revs looks like this: \ \ 0 \ -32 | +32 Overhead view of car \ \ | / \ \ | / 0 = looking straight ahead \ \|/ +64 = looking sharp right \ -64 -----+----- +64 -64 = looking sharp left \ /|\ \ / | \ \ / | \ \ -96 | +96 \ 128 \ \ So playerYawAngle is in the top-right or bottom-left \ quarter in the above diagram \ \ In both cases we jump to rotm1 to set sinYawAngle and \ cosYawAngle \ If we get here then bit 6 of J is set, so \ playerYawAngle is in one of these ranges: \ \ * 64 to 127 (%01000000 to %01111111) \ \ * -64 to -1 (%11000000 to %11111111) \ \ So playerYawAngle is in the bottom-right or top-left \ quarter in the above diagram \ \ In both cases we set the variables the other way \ round, as the triangle we draw to calculate the angle \ is the opposite way round (i.e. it's reflected in the \ x-axis or y-axis) INX \ Set X = 1 and secondAxis = 0, so we project sin(H G) DEC secondAxis \ into cosYawAngle and cos(H G) into sinYawAngle \ We now enter a loop that sets sinYawAngle + X to \ sin(H G) on the first iteration, and sets \ sinYawAngle + secondAxis to cos(H G) on the second \ iteration \ \ The commentary is for the sin(H G) iteration, see the \ end of the loop for details of how the second \ iteration calculates cos(H G) instead .rotm1 \ If we get here, then we are set up to calculate the \ following: \ \ * If playerYawAngle is top-right or bottom-left: \ \ sinYawAngle = sin(playerYawAngle) \ cosYawAngle = cos(playerYawAngle) \ \ * If playerYawAngle is bottom-right or top-left: \ \ sinYawAngle = cos(playerYawAngle) \ cosYawAngle = sin(playerYawAngle) \ \ In each case, the calculation gives us the correct \ coordinate, as the second set of results uses angles \ that are "reflected" in the x-axis or y-axis by the \ capping process in the GetAngleInRadians routine CMP #122 \ If A < 122, i.e. U < 122 and H < 122, jump to rotm2 BCC rotm2 \ to calculate sin(H G) for smaller angles BCS rotm3 \ Jump to rotm3 to calculate sin(H G) for larger angles \ (this BCS is effectively a JMP as we just passed \ through a BCS) LDA G \ It doesn't look like this code is ever reached, so CMP #240 \ presumably it's left over from development BCS rotm3 \ ****************************************************************************** \ \ Name: GetRotationMatrix (Part 2 of 5) \ Type: Subroutine \ Category: Maths (Geometry) \ Summary: Calculate sin(H G) for smaller angles \ Deep dive: The core driving model \ Trigonometry \ \ ****************************************************************************** .rotm2 \ If we get here then (U G) = yawRadians / 2 and U < 122 LDA #171 \ Set A = 171 JSR Multiply8x8 \ Set (A T) = (A * U) * U JSR Multiply8x8 \ = A * U^2 \ = 171 * (yawRadians / 2)^2 STA V \ Set (V T) = (A T) \ = 171 * (yawRadians / 2)^2 JSR Multiply8x16 \ Set (U T) = U * (V T) / 256 \ = (171 / 256) * (yawRadians / 2)^3 \ = 2/3 * (yawRadians / 2)^3 LDA G \ Set (A T) = (H G) - (U T) SEC \ = yawRadians / 2 - 2/3 * (yawRadians / 2)^3 SBC T \ STA T \ starting with the low bytes LDA H \ And then the high bytes SBC U ASL T \ Set (A T) = (A T) * 2 ROL A \ So we now have the following in (A T): \ \ (yawRadians / 2 - 2/3 * (yawRadians / 2)^3) * 2 \ \ = yawRadians - 4/3 * (yawRadians / 2)^3 \ \ = yawRadians - 4/3 * yawRadians^3 / 2^3 \ \ = yawRadians - 8/6 * yawRadians^3 * 1/8 \ \ = yawRadians - 1/6 * yawRadians^3 \ \ = yawRadians - yawRadians^3 / 3! \ \ The Taylor series expansion of sin(x) starts like \ this: \ \ sin(x) = x - (x^3 / 3!) + (x^5 / 5!) - ... \ \ If we take the first two parts of the series and \ apply them to yawRadians, we get: \ \ sin(yawRadians) = yawRadians - (yawRadians^3 / 3!) \ \ which is the same as our value in (A T) \ \ So the value in (A T) is equal to the first two parts \ of the Taylor series, and we have effectively just \ calculated an approximation of this: \ \ (A T) = sin(yawRadians) STA sinYawAngleHi,X \ Set (sinYawAngleHi sinYawAngleLo) = (A T) LDA T \ AND #%11111110 \ with the sign bit cleared in bit 0 of sinYawAngleLo to STA sinYawAngleLo,X \ denote a positive result JMP rotm5 \ Jump to rotm5 to move on to the next axis \ ****************************************************************************** \ \ Name: GetRotationMatrix (Part 3 of 5) \ Type: Subroutine \ Category: Maths (Geometry) \ Summary: Calculate sin(H G) for bigger angles \ Deep dive: The core driving model \ Trigonometry \ \ ****************************************************************************** .rotm3 \ If we get here then (H G) = yawRadians / 2 and \ H >= 122 \ PI is represented by 804, as 804 / 256 = 3.14, so 201 \ represents PI/4, which we use in the following \ subtraction LDA #0 \ Set (U T) = (201 0) - (H G) SEC \ = PI/4 - yawRadians / 2 SBC G \ STA T \ starting with the low bytes LDA #201 \ And then the high bytes SBC H STA U STA V \ Set (V T) = (U T) \ = PI/4 - yawRadians / 2 JSR Multiply8x16 \ Set (U T) = U * (V T) / 256 \ = U * (PI/4 - yawRadians / 2) \ \ U is the high byte of (U T), which also contains \ PI/4 - yawRadians / 2, so this approximation holds \ true: \ \ (U T) = U * (PI/4 - yawRadians / 2) \ =~ (PI/4 - yawRadians / 2) ^ 2 ASL T \ Set (U T) = (U T) * 2 ROL U \ = (PI/4 - yawRadians / 2) ^ 2 * 2 \ By this point we have the following: \ \ (U T) = (PI/4 - yawRadians / 2) ^ 2 * 2 \ = ((PI/2 - yawRadians) / 2) ^ 2 * 2 \ \ If we define x = PI/2 - yawRadians, then we have: \ \ (U T) = (x / 2) ^ 2 * 2 \ = ((x ^ 2) / (2 ^ 2)) * 2 \ = (x ^ 2) / 2 \ \ The small angle approximation states that for small \ values of x, the following approximation holds true: \ \ cos(x) =~ 1 - (x ^ 2) / 2! \ \ As yawRadians is large, this means x is small, so we \ can use this approximation \ \ We are storing the cosine, which is in the range 0 to \ 1, in the 16-bit variable (U T), so in terms of 16-bit \ arithmetic, the 1 in the above equation is (1 0 0) \ \ So this is the same as: \ \ cos(x) =~ (1 0 0) - (x ^ 2) / 2! \ = (1 0 0) - (U T) \ \ It's a trigonometric identity that: \ \ cos(PI/2 - x) = sin(x) \ \ so we have: \ \ cos(x) = cos(PI/2 - yawRadians) \ = sin(yawRadians) \ \ and we already calculated that: \ \ cos(x) =~ (1 0 0) - (U T) \ \ so that means that: \ \ sin(yawRadians) = cos(x) \ =~ (1 0 0) - (U T) \ \ So we just need to calculate (1 0 0) - (U T) to get \ our result LDA #0 \ Set A = (1 0 0) - (U T) SEC \ SBC T \ starting with the low bytes AND #%11111110 \ Which we store in sinYawAngleLo, with bit 0 cleared to STA sinYawAngleLo,X \ denote a positive result (as it's a sign-magnitude \ number we want to store) LDA #0 \ And then the high bytes SBC U BCC rotm4 \ We now need to subtract the top bytes, i.e. the 1 in \ (1 0 0) and the 0 in (0 U T), while including the \ carry from the high byte subtraction \ \ So the top byte should be: \ \ A = 1 - 0 - (1 - C) \ = 1 - (1 - C) \ = C \ \ If the C flag is clear, then that means the top byte \ is zero, so we already have a valid result from the \ high and low bytes, so we jump to rotm4 to store the \ high byte of the result in sinYawAngleHi \ \ If the C flag is set, then the result is (1 A T), but \ the highest possible value for sin or cos is 1, so \ that's what we return \ \ Because sinYawAngle is a sign-magnitude number with \ the sign bit in bit 0, we return the following value \ to represent the closest value to 1 that we can fit \ into 16 bits: \ \ (11111111 11111110) LDA #%11111110 \ Set sinYawAngleLo to the highest possible positive STA sinYawAngleLo,X \ value (i.e. all ones except for the sign in bit 0) LDA #%11111111 \ Set A to the highest possible value of sinYawAngleHi, \ so we can store it in the next instruction .rotm4 STA sinYawAngleHi,X \ Store A in the high byte in sinYawAngleHi \ ****************************************************************************** \ \ Name: GetRotationMatrix (Part 4 of 5) \ Type: Subroutine \ Category: Maths (Geometry) \ Summary: Loop back to calculate cos instead of sin \ Deep dive: The core driving model \ Trigonometry \ \ ****************************************************************************** .rotm5 CPX secondAxis \ If we just processed the second axis, then we have BEQ rotm6 \ now set both sinYawAngle and cosYawAngle, so jump to \ rotm6 to set their signs LDX secondAxis \ Otherwise set X = secondAxis so the next time we reach \ the end of the loop, we take the BEQ branch we just \ passed through LDA #0 \ Set (H G) = (201 0) - (H G) SEC \ SBC G \ starting with the low bytes STA G LDA #201 \ And then the high bytes SBC H STA H STA U \ Set (U G) = (H G) \ \ (U G) and (H G) were set to yawRadians / 2 for the \ first pass through the loop above, so we now have the \ following: \ \ 201 - yawRadians / 2 \ \ PI is represented by 804, as 804 / 256 = 3.14, so 201 \ represents PI/4, so this the same as: \ \ PI/4 - yawRadians / 2 \ \ Given that we expect (U G) to contain half the angle \ we are projecting, this means we are going to find the \ sine of this angle when we jump back to rotm1: \ \ PI/2 - yawRadians \ \ It's a trigonometric identity that: \ \ sin(PI/2 - x) = cos(x) \ \ so jumping back will, in fact, find the cosine of the \ angle JMP rotm1 \ Loop back to set the other variable of sinYawAngle and \ cosYawAngle to the cosine of the angle \ ****************************************************************************** \ \ Name: GetRotationMatrix (Part 5 of 5) \ Type: Subroutine \ Category: Maths (Geometry) \ Summary: Apply the correct signs to the result \ Deep dive: The core driving model \ \ ****************************************************************************** .rotm6 \ By this point, we have the yaw angle vector's \ x-coordinate in sinYawAngle and the y-coordinate in \ cosYawAngle \ \ The above calculations were done on an angle that was \ reduced to a quarter-circle, so now we need to add the \ correct signs according to which quarter-circle the \ original playerYawAngle in (J T) was in LDA J \ If J is positive then playerYawAngle is positive (as BPL rotm7 \ J contains playerYawAngleHi), so jump to rotm7 to skip \ the following \ If we get here then playerYawAngle is negative \ \ The degree system in Revs looks like this: \ \ 0 \ -32 | +32 Overhead view of car \ \ | / \ \ | / 0 = looking straight ahead \ \|/ +64 = looking sharp right \ -64 -----+----- +64 -64 = looking sharp left \ /|\ \ / | \ \ / | \ \ -96 | +96 \ 128 \ \ So playerYawAngle is in the left half of the above \ diagram, where the x-coordinates are negative, so we \ need to negate the x-coordinate LDA #1 \ Negate sinYawAngle by setting bit 0 of the low byte, ORA sinYawAngleLo \ as sinYawAngle is a sign-magnitude number STA sinYawAngleLo .rotm7 LDA J \ If bits 6 and 7 of J are the same (i.e. their EOR is ASL A \ zero), jump to rotm8 to return from the subroutine as EOR J \ the sign of cosYawAngle is correct BPL rotm8 \ Bits 6 and 7 of J, i.e. of playerYawAngleHi, are \ different, so the angle is in one of these ranges: \ \ * 64 to 127 (%01000000 to %01111111) \ \ * -128 to -65 (%10000000 to %10111111) \ \ The degree system in Revs looks like this: \ \ 0 \ -32 | +32 Overhead view of car \ \ | / \ \ | / 0 = looking straight ahead \ \|/ +64 = looking sharp right \ -64 -----+----- +64 -64 = looking sharp left \ /|\ \ / | \ \ / | \ \ -96 | +96 \ 128 \ \ So playerYawAngle is in the bottom half of the above \ diagram, where the y-coordinates are negative, so we \ need to negate the y-coordinate LDA #1 \ Negate cosYawAngle by setting bit 0 of the low byte, ORA cosYawAngleLo \ as cosYawAngle is a sign-magnitude number STA cosYawAngleLo .rotm8 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: GetAngleInRadians \ Type: Subroutine \ Category: Maths (Geometry) \ Summary: Convert a 16-bit angle into radians, restricted to a quarter \ circle \ Deep dive: Trigonometry \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ (A T) A yaw angle in Revs format (-128 to +127) \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ (U A) The angle, reduced to a quarter circle, converted to \ radians, and halved \ \ ****************************************************************************** .GetAngleInRadians ASL T \ Set (V T) = (A T) << 2 ROL A \ ASL T \ This shift multiplies (A T) by four, removing bits 6 ROL A \ and 7 in the process STA V \ \ The degree system in Revs looks like this: \ \ 0 \ -32 | +32 Overhead view of car \ \ | / \ \ | / 0 = looking straight ahead \ \|/ +64 = looking sharp right \ -64 -----+----- +64 -64 = looking sharp left \ /|\ \ / | \ \ / | \ \ -96 | +96 \ 128 \ \ The top byte of (A T) is in this range, so shifting to \ the left by two places drops bits 6 and 7 and scales \ the angle into the range 0 to 252, as follows: \ \ * 0 to 63 (%00000000 to %00111111) \ -> 0 to 252 (%00000000 to %11111100) \ \ * 64 to 127 (%01000000 to %01111111) \ -> 0 to 252 (%00000000 to %11111100) \ \ * -1 to -64 (%11111111 to %11000000) \ -> 252 to 0 (%11111100 to %00000000) \ \ * -65 to -128 (%10111111 to %10000000) \ -> 252 to 0 (%11111100 to %00000000) \ We now convert this number from a Revs angle into \ radians \ \ The value of (V T) represents a quarter-circle, which \ is PI/2 radians, but we actually multiply by PI/4 to \ return the angle in radians divided by 2, to prevent \ overflow in the GetRotationMatrix routine LDA #201 \ Set U = 201 STA U \ Fall through into Multiply8x16 to calculate: \ \ (U A) = U * (V T) / 256 \ = 201 * (V T) / 256 \ = (201 / 256) * (V T) \ = (3.14 / 4) * (V T) \ \ So we return (U A) = PI/4 * (V T) \ \ which is the original angle, reduced to a quarter \ circle, converted to radians, and halved \ ****************************************************************************** \ \ Name: Multiply8x16 \ Type: Subroutine \ Category: Maths (Arithmetic) \ Summary: Multiply an 8-bit and a 16-bit number \ \ ------------------------------------------------------------------------------ \ \ Do the following multiplication of two unsigned numbers: \ \ (U T) = U * (V T) / 256 \ \ The result is also available in (U A). \ \ ****************************************************************************** .Multiply8x16 JSR Multiply8x8+2 \ Set (A T) = T * U STA W \ Set (W T) = (A T) \ = T * U \ \ So W = T * U / 256 LDA V \ Set A = V JSR Multiply8x8 \ Set (A T) = A * U \ = V * U STA U \ Set (U T) = (A T) \ = V * U LDA W \ Set (U T) = (U T) + W CLC \ ADC T \ starting with the low bytes STA T BCC mult1 \ And then the high bytes, so we get the following: INC U \ \ (U T) = (U T) + W \ = V * U + (T * U / 256) \ = U * (V + T / 256) \ = U * (256 * V + T) / 256 \ = U * (V T) / 256 \ \ which is what we want .mult1 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: Multiply16x16 \ Type: Subroutine \ Category: Maths (Arithmetic) \ Summary: Multiply a sign-magnitude 16-bit number and a signed 16-bit number \ \ ------------------------------------------------------------------------------ \ \ This routine calculates: \ \ (A T) = (QQ PP) * (SS RR) / 256^2 \ \ It uses the following algorithm: \ \ (QQ PP) * (SS RR) = (QQ << 8 + PP) * (SS << 8 + RR) \ = (QQ << 8 * SS << 8) + (QQ << 8 * RR) \ + (PP * SS << 8) \ + (PP * RR) \ = (QQ * SS) << 16 + (QQ * RR) << 8 \ + (PP * SS) << 8 \ + (PP * RR) \ \ Finally, it replaces the low byte multiplication in (PP * RR) with 128, as an \ estimate, as it's a pain to multiply the low bytes of a signed integer with a \ sign-magnitude number. So the final result that is returned in (A T) is as \ follows: \ \ (A T) = (QQ PP) * (SS RR) / 256^2 \ = ((QQ * SS) << 16 + (QQ * RR) << 8 + (PP * SS) << 8 + 128) / 256^2 \ \ which is the algorithm that is implemented in this routine. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ (QQ PP) 16-bit signed integer \ \ (SS RR) 16-bit sign-magnitude integer with the sign bit in bit 0 \ of RR \ \ H The sign to apply to the result (in bit 7) \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ (A T) (QQ PP) * (SS RR) * abs(H) \ \ ****************************************************************************** .Multiply16x16 LDA QQ \ If (QQ PP) is positive, jump to muls1 to skip the BPL muls1 \ following LDA #0 \ (QQ PP) is negative, so we now negate (QQ PP) so it's SEC \ positive, starting with the low bytes SBC PP STA PP LDA #0 \ And then the high bytes SBC QQ \ STA QQ \ So we now have (QQ PP) = |QQ PP| LDA H \ Flip bit 7 of H, so when we set the result to the sign EOR #%10000000 \ of H below, this ensures the result is the correct STA H \ sign .muls1 LDA RR \ If bit 0 of RR is clear, then (SS RR) is positive, so AND #1 \ jump to muls2 BEQ muls2 LDA H \ Flip bit 7 of H, so when we set the result to the sign EOR #%10000000 \ of H below, this ensures the result is the correct STA H \ sign .muls2 LDA QQ \ Set U = QQ STA U LDA RR \ Set A = RR JSR Multiply8x8 \ Set (A T) = A * U \ = RR * QQ STA W \ Set (W T) = (A T) \ = RR * QQ LDA T \ Set (W V) = (A T) + 128 CLC \ = RR * QQ + 128 ADC #128 \ STA V \ starting with the low bytes BCC muls3 \ And then the high byte INC W \ So we now have (W V) = RR * QQ + 128 .muls3 LDA SS \ Set A = SS JSR Multiply8x8 \ Set (A T) = A * U \ = SS * QQ STA G \ Set (G T) = (A T) \ = SS * QQ LDA T \ Set (G W V) = (G T 0) + (W V) CLC \ ADC W \ starting with the middle bytes (as the low bytes are STA W \ simply V = 0 + V with no carry) BCC muls4 \ And then the high byte INC G \ So now we have: \ \ (G W V) = (G T 0) + (W V) \ = (SS * QQ << 8) + RR * QQ + 128 .muls4 LDA PP \ Set U = PP STA U LDA SS \ Set A = SS JSR Multiply8x8 \ Set (A T) = A * U \ = SS * PP STA U \ Set (U T) = (A T) \ = SS * PP LDA T \ Set (G T ?) = (G W V) + (U T) CLC \ ADC V \ starting with the low bytes (which we throw away) LDA U \ And then the high bytes ADC W STA T BCC muls5 \ And then the high byte INC G \ So now we have: \ \ (G T ?) = (G W V) + (U T) \ = (SS * QQ << 8) + RR * QQ + 128 + SS * PP \ = (QQ * SS) << 8 + (QQ * RR) + (PP * SS) \ + 128 \ = (QQ PP) * (SS RR) / 256 \ \ So: \ \ (G T) = (G T ?) / 256 \ = (QQ PP) * (SS RR) / 256^2 \ \ which is the result that we want .muls5 LDA G \ Set (A T) = (G T) BIT H \ We are about to fall through into Absolute16Bit, so \ this ensures we set the sign of (A T) to the sign in \ H, so we get: \ \ (A T) = (A T) * abs(H) \ ****************************************************************************** \ \ Name: Absolute16Bit \ Type: Subroutine \ Category: Maths (Arithmetic) \ Summary: Calculate the absolute value (modulus) of a 16-bit number \ \ ------------------------------------------------------------------------------ \ \ This routine sets (A T) = |A T|. \ \ It can also return (A T) * abs(n), where A is given the sign of n. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ (A T) The number to make positive \ \ N flag Controls the sign to be applied: \ \ * If we want to calculate |A T|, do an LDA or \ equivalent before calling the routine \ \ * If we want to calculate (A T) * abs(n), do a BIT n \ before calling the routine \ \ * If we want to set the sign of (A T), then call with: \ \ * N flag clear to calculate (A T) * 1 \ \ * N flag set to calculate (A T) * -1 \ \ ****************************************************************************** .Absolute16Bit BPL ScanKeyboard-1 \ If the high byte in A is already positive, return from \ the subroutine (as ScanKeyboard-1 contains an RTS) \ Otherwise fall through into Negate16Bit to negate the \ number in (A T), which will make it positive, so this \ sets (A T) = |A T| \ ****************************************************************************** \ \ Name: Negate16Bit \ Type: Subroutine \ Category: Maths (Arithmetic) \ Summary: Negate a 16-bit number \ \ ------------------------------------------------------------------------------ \ \ This routine negates the 16-bit number (A T). \ \ ------------------------------------------------------------------------------ \ \ Other entry points: \ \ Negate16Bit+2 Set (A T) = -(U T) \ \ ****************************************************************************** .Negate16Bit STA U \ Set (U T) = (A T) LDA #0 \ Set (A T) = 0 - (U T) SEC \ = -(A T) SBC T \ STA T \ starting with the low bytes LDA #0 \ And then the high bytes SBC U RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: ScanKeyboard \ Type: Subroutine \ Category: Keyboard \ Summary: Scan the keyboard for a specific key press \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The negative inkey value of the key to scan for (in the \ range &80 to &FF) \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ Z flag The result: \ \ * Set if the key in X is being pressed, in which case \ BEQ will branch \ \ * Clear if the key in X is not being pressed, in which \ case BNE will branch \ \ ------------------------------------------------------------------------------ \ \ Other entry points: \ \ ScanKeyboard-1 Contains an RTS \ \ ****************************************************************************** .ScanKeyboard LDA #129 \ Call OSBYTE with A = 129, Y = &FF and the inkey value LDY #&FF \ in X, to scan the keyboard for key X JSR OSBYTE CPX #&FF \ If the key in X is being pressed, the above call sets \ both X and Y to &FF, so this sets the Z flag depending \ on whether the key is being pressed (so a BEQ after \ the call will branch if the key in X is being pressed) RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: FlushSoundBuffer \ Type: Subroutine \ Category: Sound \ Summary: Flush the specified sound buffer \ Deep dive: The engine sounds \ \ ------------------------------------------------------------------------------ \ \ This routine flushes the specified sound channel buffer, but only if that \ channel's soundBuffer value is non-zero. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The number of the sound channel buffer to flush (0 to 3) \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ X X is unchanged \ \ A A is unchanged \ \ ****************************************************************************** .FlushSoundBuffer PHA \ Store the value of A on the stack so we can retrieve \ it before returning from the routine LDA soundBuffer,X \ If this buffer's soundBuffer value is zero, then there BEQ flus1 \ is nothing to flush, so jump to flus1 to return from \ the subroutine LDA #0 \ Set this buffer's soundBuffer value for this buffer to STA soundBuffer,X \ 0 to indicate that it has been flushed TXA \ Set bit 2 of X ORA #%00000100 \ TAX \ This changes X from the original range of 0 to 3, into \ the range 4 to 7, so it now matches the relevant sound \ buffer number (as buffers 4 to 7 are the buffers for \ sound channels 0 to 3) LDA #21 \ Call OSBYTE with A = 21 to flush buffer X, which JSR OSBYTE \ flushes the relevant sound channel buffer TXA \ Clear bit 2 of X, to reverse the X OR 4 above AND #%11111011 TAX .flus1 PLA \ Retrieve the value of A that we stored on the stack \ above, so it remains unchanged by the routine RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: MakeDrivingSounds \ Type: Subroutine \ Category: Sound \ Summary: Make the relevant sounds for the engine and tyres \ Deep dive: The engine sounds \ \ ****************************************************************************** .MakeDrivingSounds LDA tyreSqueal \ If bit 7 of tyreSqueal is clear for both tyres, jump ORA tyreSqueal+1 \ to soun1 to skip the following as no tyres are BPL soun1 \ squealing \ Otherwise we add some random pitch variation to the \ crash/contact sound and make the sound of the tyres \ squealing LDA VIA+&68 \ Read 6522 User VIA T1C-L timer 2 low-order counter \ (SHEILA &68), which decrements one million times a \ second and will therefore be pretty random CMP #63 \ If A < 63 (25% chance), jump to soun1 to skip the BCS soun1 \ following AND #3 \ Reduce A to a random number in the range 0 to 3 CLC \ Add 130 to A, so A is a random number in the range ADC #130 \ 130 to 133 STA soundData+28 \ Update byte #5 of sound #4 (low byte of pitch) so the \ pitch of the crash/contact sound wavers randomly LDA #3 \ Make sound #3 (tyre squeal) using envelope 1 LDY #1 JSR MakeSound .soun1 \ We now increment or decrement soundRevCount so it \ steps towards the value of soundRevTarget, which moves \ the pitch of the engine towards the current rev count LDX soundRevCount \ Set X = soundRevCount CPX soundRevTarget \ If X = soundRevTarget, jump to soun8 to return from BEQ soun8 \ the subroutine BCC soun2 \ If X < soundRevTarget, jump to soun2 to increment X DEX \ Decrement X and skip the next instruction (this BCS BCS soun3 \ is effectively a JMP as we passed through the BCC) .soun2 INX \ Increment X .soun3 STX soundRevCount \ Store X in soundRevCount, so soundRevCount moves one \ step closer to soundRevTarget \ We now do the following, depending on the updated \ value of soundRevCount in X: \ \ * If soundRevCount < 28, flush all the sound buffers \ (i.e. stop making any sounds) \ \ * If 28 <= soundRevCount < 92, make the engine \ exhaust sound, set the pitch of engine tone 1 to \ soundRevCount + 95 and the volume of engine tone 1 \ to 0, and make the sound of engine tone 1 \ \ * If soundRevCount >= 92, silence the exhaust, set \ the pitch of engine tone 1 to soundRevCount - 92, \ and make the sound of engine tone 1 CPX #28 \ If X < 28, then jump to soun9 to flush all the sound BCC soun9 \ buffers, as the rev count is too low for the engine to \ make a sound TXA \ Set A = X - 92 SEC SBC #92 BCS soun4 \ If the subtraction didn't underflow, i.e. X >= 92, \ then jump to soun4 to silence the engine exhaust and \ set the pitch of engine tone 1 to X - 92 PHA \ Store A on the stack to we can retrieve it after the \ following call LDA #0 \ Make sound #0 (engine exhaust) at the current volume JSR MakeSound-3 \ level PLA \ Retrieve the value of A that we stored on the stack, \ so A = X - 92 CLC \ Set A = A + 187 ADC #187 \ = X - 92 + 187 \ = X + 95 \ \ so we set the pitch of engine tone 1 to X + 95 LDY #0 \ Set Y = 0, so we set the volume of engine tone 1 to \ zero (silent) BEQ soun5 \ Jump to soun5 (this BEQ is effectively a JMP as Y is \ always zero) .soun4 LDX #0 \ Flush the buffer for sound channel 0, which will stop JSR FlushSoundBuffer \ the sound of the engine exhaust LDY volumeLevel \ Set Y to the current volume level .soun5 STA soundData+12 \ Update byte #5 of sound #1 (low byte of pitch), to set \ the pitch of engine tone 1 to A LDA #1 \ Make sound #1 (engine tone 1) with volume Y JSR MakeSound \ We now do the following, depending on the updated \ value of soundRevCount: \ \ * If the volume level is currently zero, make the \ sound of engine tone 2 sound with volume 0 \ \ * If soundRevCount >= 64, set the pitch of engine \ tone 2 to soundRevCount - 64, and make the sound \ of engine tone 2 \ \ * If soundRevCount < 64, make the sound of engine \ tone 2 with volume 0 LDY volumeLevel \ If the volume level is currently zero (no sound), jump BEQ soun7 \ to soun7 to make the engine tone 2 sound with volume 0 LDA soundRevCount \ Set A = soundRevCount - 64 SEC SBC #64 BCS soun6 \ If the subtraction didn't underflow, i.e. A >= 64, \ then jump to soun6 to set the pitch of engine tone 2 \ to soundRevCount - 64 LDY #0 \ Set Y = 0, so we set the volume of engine tone 2 to \ zero (silent) BEQ soun7 \ Jump to soun7 (this BEQ is effectively a JMP as Y is \ always zero) .soun6 STA soundData+20 \ Update byte #5 of sound #2 (low byte of pitch), to set \ the pitch of engine tone 2 to A .soun7 LDA #2 \ Make sound #2 (engine tone 2) with volume Y JSR MakeSound .soun8 RTS \ Return from the subroutine .soun9 JSR FlushSoundBuffers \ Flush all four sound channel buffers RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: ProcessShiftedKeys \ Type: Subroutine \ Category: Keyboard \ Summary: Check for shifted keys (i.e. those that need SHIFT holding down to \ trigger) and process them accordingly \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ Y Scan for the first Y + 1 keys from shiftedKeys \ \ ****************************************************************************** .ProcessShiftedKeys STY T \ Set T to the number of keys to scan LDX #&FF \ Scan the keyboard to see if SHIFT is being pressed JSR ScanKeyboard BNE shif10 \ If SHIFT is not being pressed, jump to shif10 to \ return from the subroutine LDY T \ Set Y to the number of keys to scan, to use as a loop \ counter as we work our way backwards through the \ shiftedKeys table, from entry Y to entry 0 .shif1 STY T \ Set T to the loop counter LDX shiftedKeys,Y \ Fetch the next key number from the shiftedKeys table JSR ScanKeyboard \ Scan the keyboard to see if this key is being pressed BEQ shif2 \ If this key is being pressed, jump to shif2 to update \ the relevant configuration setting LDY T \ Otherwise set Y to the value of the loop counter DEY \ Decrement the loop counter to point to the next key in \ the table (working backwards) BPL shif1 \ Loop back to check the next key in the table until we \ have checked them all BMI shif3 \ None of the keys are being pressed, so jump to shif3 \ to skip updating the configuration bytes (this BMI is \ effectively a JMP as we just passed through a BPL) .shif2 \ If we get here then the Y-th key is being pressed, \ along with SHIFT, so we now update the relevant \ configuration byte, according to the settings in the \ configKeys table LDY T \ Otherwise set Y to the value of the loop counter, \ which gives us the offset of key that is being pressed \ within the shiftedKeys table LDA configKeys,Y \ Set X to the low nibble for this key's corresponding AND #&0F \ entry in the configKeys, which contains the offset of TAX \ the configuration byte from the first configuration \ byte at configStop LDA configKeys,Y \ Set A to the high nibble for this key's corresponding AND #&F0 \ entry in the configKeys, which contains the value that \ we need for the corresponding configuration byte STA configStop,X \ Set the corresponding configuration byte to the value \ in A .shif3 LDA configPause \ If configPause = 0, then neither COPY nor DELETE are BEQ shif6 \ being, so jump to shif6 \ If we get here then one of the pause buttons is being \ pressed BPL shif5 \ If bit 7 of configPause is clear, then this means bit \ 6 must be set, which only happens when the unpause key \ (DELETE) is being pressed, so jump to shif5 to unpause \ the game \ Otherwise we need to pause the game JSR FlushSoundBuffers \ Flush all four sound channel buffers to stop the sound \ while we are paused .shif4 JSR ResetTrackLines \ Reset the blocks at leftVergeStart, leftTrackStart, \ rightVergeStart, rightGrassStart and backgroundColour LDX #&A6 \ Scan the keyboard to see if DELETE is being pressed JSR ScanKeyboard BNE shif4 \ If DELETE is not being pressed, loop back to shif4 to \ remain paused, otherwise keep going to unpause the \ game .shif5 INC soundRevCount \ Increment soundRevCount to make the engine sound jump \ a little LDA #0 \ Set configPause = 0 to clear the pause/unpause key STA configPause \ press .shif6 LDY volumeLevel \ Set Y to the volume level, which uses the operating \ system's volume scale, with -15 being full volume and \ 0 being silent LDA mainLoopCounterLo \ If bit 0 of mainLoopCounterLo is set, which it will be AND #1 \ every other iteration round the main loop, jump to BNE shif9 \ shif9 to skip the following, so the sound changes more \ slowly than it would if we did this every loop LDA configVolume \ If configVolume = 0, jump to shif10 to return from the BEQ shif10 \ subroutine BPL shif7 \ If bit 7 of configVolume is clear, then this means bit \ 6 must be set, which only happens when the volume up \ (f5) key is being pressed, so jump to shif7 \ If we get here then we need to turn the volume down INY \ Increment Y to decrease the volume BEQ shif8 \ If Y is 0 or negative, then it is still a valid volume BMI shif8 \ level, so jump to shif8 to update the volume setting BPL shif9 \ Otherwise we have already turned the volume down as \ far as it will go, so jump to shif9 to clear the key \ press and return from the subroutine (this BPL is \ effectively a JMP as we just passed through a BMI) .shif7 \ If we get here then we need to turn the volume up DEY \ Decrement Y to increase the volume CPY #241 \ If Y < -15, then we have already turned the volume up BCC shif9 \ as far as it will go, so jump to shif9 to clear the \ key press and return from the subroutine \ Otherwise fall through into shif8 to update the \ volume setting .shif8 STY volumeLevel \ Store the updated volume level in volumeLevel TYA \ Set A = -Y, negated using two's complement EOR #&FF CLC ADC #1 ASL A \ Set envelopeData+12 = A << 3 ASL A \ = -Y * 8 ASL A \ STA envelopeData+12 \ which is 0 for no volume, or 120 for full volume, so \ this sets the target level for the end of the attack \ phase to a higher figure for higher volume settings LDA #0 \ Set up the envelope for the engine sound, with the JSR DefineEnvelope \ volume changed accordingly INC soundRevCount \ Increment soundRevCount to make the engine sound jump \ a little .shif9 LDA #0 \ Set configVolume = 0 to clear the volume key press STA configVolume .shif10 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: SortDrivers \ Type: Subroutine \ Category: Drivers \ Summary: Create a sorted list of driver numbers, ordered as specified \ \ ------------------------------------------------------------------------------ \ \ This routine sorts the driver list in driversInOrder according to the value \ specified by argument A. It also populates carStatus with the position \ numbers for the sorted driver list, which will typically run from 0 to 19, \ but may also contain repeated numbers in the case of a tie. \ \ The routine uses a basic bubble sort algorithm, swapping neighbouring drivers \ repeatedly until the whole list is sorted. This is not very efficient, but as \ this is only done when showing the driver table between races, that doesn't \ matter. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ A Determines the order of the sorted list to create: \ \ * 0 = best lap times \ \ * Bit 6 set = accumulated points \ \ * Bit 7 set = total race times \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ positionNumber A list of position numbers, from 0 to 19, ready to print \ in the first column of the driver table (with 1 added), \ with drivers who are tied in the same position sharing \ the same number \ \ driversInOrder A list of driver numbers, sorted according to the value \ specified by argument A \ \ ****************************************************************************** .SortDrivers STA G \ Store A in G SED \ Set the D flag to switch arithmetic to Binary Coded \ Decimal (BCD) .sort1 LDX #0 \ Set V = 0, which we will use to indicate whether the STX V \ driversInOrder list is sorted \ \ We start at 0 to indicate it is sorted, and change it \ if we have to reorder the list STX positionNumber \ Set the first entry in positionNumber to 0, as the \ winning driver will always be in position 0 INX \ Set X = 1 as a position counter, counting through 1 to \ 19, which denotes the position number that we are \ processing in this iteration of the loop (we skip the \ first position as we already set it) .sort2 STX W \ Store the position counter in W LDY driversInOrder,X \ Set Y to the number of the driver at position X in the \ driversInOrder list ("this driver") TXA \ Set the X-th entry in positionNumber to the position STA positionNumber,X \ counter (as the X-th driver is in position X) LDA driversInOrder-1,X \ Set X to the number of the driver at position X - 1 in TAX \ the driversInOrder list ("the driver ahead") SEC \ Set the C flag for the subtractions below BIT G \ If bit 6 of G is set, jump to sort5 to compare total BVS sort5 \ points BMI sort6 \ If bit 7 of G is set, jump to sort6 to compare best \ lap times \ If we get here then bit 6 and 7 of G are clear, so we \ compare most recent lap times LDA bestLapTenths,Y \ Set (A H U) = this driver's best lap time SBC bestLapTenths,X \ - best lap time of the driver ahead STA U \ \ starting with the tenths of a second LDA bestLapSeconds,Y \ Then the seconds SBC bestLapSeconds,X STA H LDA bestLapMinutes,Y \ And then the minutes SBC bestLapMinutes,X BCC sort7 \ If the subtraction underflowed, then this driver's \ lap time is quicker than the lap time of the driver \ ahead, which is the wrong way round if we are trying \ to create a list where the winner has the fastest lap \ time, so jump to sort7 to swap them around in the \ driversInOrder list .sort3 ORA U \ At this point (A H U) contains the difference between ORA H \ the two drivers' times/points, so this jumps to sort4 BNE sort4 \ if any of the bytes in (A U H) are non-zero, i.e. if \ the two drivers have different times/points LDX W \ The two drivers have identical times/points, so set DEX \ we need to set the current driver's position number to LDA positionNumber,X \ be the same as the position number of the driver ahead STA positionNumber+1,X \ as there is a tie .sort4 \ If we get here then we move on to the next position LDX W \ Fetch the position counter that we stored in W above INX \ Increment the position counter to the next position CPX #20 \ Loop back until we have gone through the whole table BCC sort2 \ of 20 positions LDA V \ If V <> 0 then we had to alter the order of the BNE sort1 \ driversInOrder list, as it wasn't fully sorted, so \ we jump back to sort1 to repeat the whole process as \ we don't yet know that the list is fully sorted CLD \ Otherwise the driversInOrder list is sorted, so clear \ the D flag to switch arithmetic to normal JSR SetPlayerPositions \ Set the current player's position, plus the position \ ahead and the position behind RTS \ Return from the subroutine .sort5 LDA totalPointsLo,X \ Set (A H U) = total points of the driver ahead SBC totalPointsLo,Y \ - this driver's total points STA U \ \ starting with the low bytes LDA totalPointsHi,X \ Then the high bytes SBC totalPointsHi,Y STA H LDA totalPointsTop,X \ And then the top bytes SBC totalPointsTop,Y BCC sort7 \ If the subtraction underflowed, then this driver has \ more points than the driver ahead, which is the wrong \ way round if we are trying to create a list where the \ winner has the most points, so jump to sort7 to swap \ them around in the driversInOrder list BCS sort3 \ Jump to sort3 to check for a tie and move on to the \ next position (this BCS is effectively a JMP as we \ just passed through a BCC) .sort6 LDA totalRaceTenths,Y \ Set (A H U) = this driver's total race time SBC totalRaceTenths,X \ - total race time of the driver ahead STA U \ \ starting with the tenths of a second LDA totalRaceSeconds,Y \ Then the seconds SBC totalRaceSeconds,X STA H LDA totalRaceMinutes,Y \ And then the minutes SBC totalRaceMinutes,X BCS sort4 \ If the subtraction didn't underflow then the drivers \ are in the correct order, so jump to sort4 to move on \ to the next position \ Otherwise the subtraction underflowed, so this \ driver's total race time is quicker than the total \ race time of the driver ahead, which is the wrong way \ round if we are trying to create a list where the \ winner has the fastest time, so fall through into \ sort7 to swap them around in the driversInOrder list .sort7 \ If we get here then the two drivers we are comparing \ are in the wrong order in the driversInOrder list, so \ we need to swap them round \ \ At this point X contains the number of the driver \ ahead and Y contains the number of this driver STX T \ Store the number of the driver ahead in T LDX W \ Set X to the position counter TYA \ Set A to the number of this driver STA driversInOrder-1,X \ Set the number of the driver ahead (i.e. the position \ before the one we are processing) to A (i.e. the \ number of this driver) LDA T \ Set the number of this driver (i.e. the current STA driversInOrder,X \ position) to T (i.e. the number of the driver ahead) DEC V \ Decrement V so that is it non-zero, to indicate that \ we had to swap an entry in the driversInOrder list JMP sort4 \ Jump to sort4 to move on to the next position \ ****************************************************************************** \ \ Name: UpdateLapTimers \ Type: Subroutine \ Category: Drivers \ Summary: Update the lap timers and display timer-related messages at the \ top of the screen \ \ ****************************************************************************** .UpdateLapTimers LDA raceStarted \ If bit 7 of raceStarted is clear then this is either BPL laps2 \ a practice or qualifying lap, so jump to laps2 to \ update the lap timers for qualifying \ If we get here then this is a race lap BIT updateDrivingInfo \ If bit 7 of updateDrivingInfo is clear then we do not BPL laps1 \ need to update the lap number, so jump to laps1 to \ skip straight to updating the driver positions LDA #%00000000 \ Clear bits 6 and 7 of updateDrivingInfo so we don't STA updateDrivingInfo \ update the number of laps again until the value of \ updateDrivingInfo changes to indicate that we should STA G \ Set G = 0 so the call to Print2DigitBCD below will \ print the second digit and will not print leading \ zeroes when printing the number of laps LDX currentPlayer \ Set X to the driver number of the current player LDA driverLapNumber,X \ Set A to the current lap number for the current player CMP #1 \ If A >= 1, set the C flag, otherwise clear it EOR #&FF \ Set A = numberOfLaps + ~A + C ADC numberOfLaps \ = numberOfLaps - A if A >= 1 \ = numberOfLaps - 1 if A = 0 PHP \ Store the resulting flags on the stack JSR ConvertNumberToBCD \ Convert the number in A into binary coded decimal \ (BCD), adding 1 in the process LDX #12 \ Print the number in A at column 12, pixel row 33, on LDY #33 \ the second text line at the top of the screen JSR Print2DigitBCD-6 PLP \ If the result of the above addition was positive, jump BPL laps1 \ to laps1 to skip printing the finished message LDX #53 \ Blank out the first text line at the top of the screen JSR PrintSecondLineGap \ and print token 53 on the second line, to give: \ \ " " \ " FINISHED " .laps1 LDA leaveTrackTimer \ If leaveTrackTimer is non-zero then the leave track BNE laps8 \ timer is counting down, so jump to laps8 to return \ from the subroutine without updating the text at the \ top of the screen JSR UpdatePositionInfo \ Otherwise update the position number and driver names \ at the top of the screen RTS \ Return from the subroutine .laps2 \ If we get here then this is a practice or qualifying \ lap LDX #1 \ Add time to the lap timer at (lapMinutes lapSeconds JSR AddTimeToTimer \ lapTenths), setting the C flag if the time has changed BIT updateDrivingInfo \ If bit 6 of updateDrivingInfo is set then we have BVS laps4 \ started the first lap, so jump to laps4 to skip the \ following and print the lap time only (we make the \ jump with the C flag indicating whether the timer has \ changed) BPL laps6 \ If bit 7 of updateDrivingInfo is clear then we do not \ need to update the lap time, so jump to laps6 to skip \ the following \ If we get here then we have started the first lap of \ practice or qualifying and we need to print the lap \ time LSR updateDrivingInfo \ Bit 7 of updateDrivingInfo is set and bit 6 is clear, \ so clear bit 7 and set bit 6 of updateDrivingInfo to \ indicate that we are now driving the first lap LDA #33 \ Set firstLapStarted = firstLapStarted + 33 CLC \ ADC firstLapStarted \ So if we have just started the first lap, then this STA firstLapStarted \ changes firstLapStarted from -33 to 0 BEQ laps3 \ If A = 0, then we just started the first qualifying or \ practice lap, so jump to laps3 to skip the following \ two instructions \ I am not sure if we ever get here, as the current lap \ time is never printed with tenths of a second LDA #%00100110 \ Print the current lap time at the top of the screen in JSR PrintLapTime+2 \ the following format: \ \ * %00 Minutes: No leading zeroes, print both digits \ * %10 Seconds: Leading zeroes, print both digits \ * %0 Tenths: Print tenths of a second \ * %11 Tenths: Leading zeroes, no second digit .laps3 LDX #1 \ Zero the lap timer JSR ZeroTimer BEQ laps6 \ Jump to laps6 (this BNE is effectively a JMP as the \ ZeroTimer routine sets the Z flag) .laps4 \ If we get here, then the C flag indicates whether the \ lap timer at (lapMinutes lapSeconds lapTenths) has \ changed LDA firstLapStarted \ If firstLapStarted = 0 then we are currently driving BEQ laps5 \ the first qualifying or practice lap, so jump to laps5 \ to print the current lap time, but not the best lap \ time (as we haven't completed a lap yet) \ \ We jump with the C flag indicating whether the timer \ has changed DEC firstLapStarted \ Decrement firstLapStarted BNE laps6 \ If firstLapStarted is non-zero, jump to laps6 to skip \ printing any lap times JSR PrintBestLapTime \ Print the best lap time and the current lap time at \ the top of the screen LDA #2 \ Print two spaces JSR PrintSpaces BEQ laps6 \ Jump to laps6 to skip the following (this BEQ is \ effectively a JMP, as PrintSpaces sets the Z flag) .laps5 \ If we get here, then the C flag indicates whether the \ lap timer at (lapMinutes lapSeconds lapTenths) has \ changed BCC laps6 \ If the C flag is clear then the timer has not changed, \ so jump to laps6 to skip the following instruction JSR PrintLapTime \ Print the current lap time at the top of the screen in \ the following format: \ \ * Minutes: No leading zeroes, print both digits \ * Seconds: Leading zeroes, print both digits \ * Tenths: Do not print tenths of a second .laps6 LDA qualifyingTime \ If bit 7 of qualifyingTime is set then this is a BMI laps8 \ practice lap (i.e. qualifyingTime = 255), so jump to \ laps8 to return from the subroutine \ If we get here then this is a qualifying lap, and the \ number of minutes of qualifying lap time is in A, as \ follows: \ \ * A = 4 indicates 5 minutes of qualifying time \ \ * A = 9 indicates 10 minutes of qualifying time \ \ * A = 25 indicates 26 minutes of qualifying time CMP clockMinutes \ If A < clockMinutes then we have reached the end of BCC laps7 \ qualifying time, so jump to laps7 to display the \ time-up message BNE laps8 \ If A <> clockMinutes, i.e. A > clockMinutes, then \ there is still some qualifying time left, so jump to \ laps8 to return from the subroutine BIT qualifyTimeEnding \ If bit 6 of qualifyTimeEnding is set, then we have BVS laps8 \ already displayed the one-minute warning, so jump to \ laps8 to return from the subroutine LDA #%01000000 \ Set bit 6 of qualifyTimeEnding to indicate that the STA qualifyTimeEnding \ one-minute warning has been displayed LDX #41 \ Print token 41 on the first text line at the top of JSR PrintFirstLine \ the screen, to give: \ \ " Less than one minute to go " RTS \ Return from the subroutine .laps7 LDA qualifyTimeEnding \ If bit 7 of qualifyTimeEnding is set, then we have BMI laps8 \ already displayed the time-up message, so jump to \ laps8 to return from the subroutine LDA #%11000000 \ Set bits of 6 and 7 of qualifyTimeEnding to indicate STA qualifyTimeEnding \ that we have displayed both the one-minute warning and \ the time-up message LDA #60 \ Set leaveTrackTimer = 60, so we leave the track in 60 STA leaveTrackTimer \ main loop iterations and return to the game menu LDX #42 \ Print token 42 on the first text line at the top of JSR PrintFirstLine \ the screen, to give: \ \ " YOUR TIME IS UP! " .laps8 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: PlaceCarsOnTrack \ Type: Subroutine \ Category: Car geometry \ Summary: Position the cars on the track, ready for a race or qualifying lap \ \ ------------------------------------------------------------------------------ \ \ This routine places the cars on the track, correctly spaced out and on \ alternating sides, and populates the track segment buffer so the first entry \ is 32 segments in front of the current player, and the whole buffer is full \ of buffered segments. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ A The distance between each car: \ \ * 1 if this is a race \ \ * The value of trackCarSpacing if this is practice or \ qualifying (40 for Silverstone) \ \ ****************************************************************************** .PlaceCarsOnTrack STA V \ Store the value of A in V, so V contains the size of \ the gap that we want to insert between the cars SEC \ Set bit 7 of updateLapTimes, so when we move the cars ROR updateLapTimes \ forward in MoveObjectForward, the lap number and lap \ time are not affected \ We start by incrementing the segment numbers for each \ car, to "move" the cars along the track, stopping when \ driver 0 is on segment 0 (i.e. when it has reached the \ starting line) \ \ Before the call to PlaceCarsOnTrack, we set each car's \ objectSegment to the value of trackStartLine, which is \ 843 for Silverstone \ \ The following loop moves all the cars forwards one \ segment at a time, until objectSegment wraps round to \ zero (which it does when it reaches the value of \ trackLength, which is 1024 for Silverstone, so the \ segment numbers go from 843 to 1023 and then to 0) \ \ In other words, the following moves the cars forward \ by 1024 - 843 segments from the start of section 0, \ or 181 segments, so the starting line at Silverstone \ is at segment 181, and this value is defined by the \ trackStartLine value in the track data file .rcar1 LDX #19 \ Set a loop counter in X to loop through the drivers .rcar2 JSR MoveObjectForward \ Move driver X forwards by one segment DEX \ Decrement the loop counter BPL rcar2 \ Loop back until we have processed all 20 drivers LDA objectSegmentLo \ If objectSegment for driver 0 is non-zero, jump back ORA objectSegmentHi \ to rcar1 to repeat the above loop BNE rcar1 \ The drivers are now all at the starting line (as they \ all started out at the same place before the above \ moves) LDA #&FF \ Set G = -1, so it can be incremented to 0 as the start STA G \ of the outer loop at rcar5 \ We now jump into a nested loop, with an inner loop \ between rcar3 and rcar5 an outer loop between rcar3 \ and rcar6 \ \ We iterate round the outer loop with G from 0 to 19, \ and for each outer iteration, we iterate round the \ inner loop with X from G to 19 \ \ The outer loop runs through each position, while the \ inner loop runs through each car behind that position, \ so the cars get moved backwards around the track as a \ group, with one car being dropped off after each inner \ loop in the correct position \ \ Specifically, the first iteration of the inner loop \ moves the cars in positions 0 to 19 backwards, then \ the cars in positions 1 to 19, then 2 to 19 and so on, \ leaving a trail of cars behind it as it works back \ along the track, so the car in position 0 is first, \ then the car in position 1, and so on to position 19 BNE rcar5 \ Jump to rcar5 (this BNE is effectively a JMP as A is \ never zero) .rcar3 \ This is the start of the inner loop, which runs \ through each position from G to 19, moving each car \ backwards V times (so each car moves backwards by the \ distance in V) LDA V \ Set W = V, so W contains the size of the gap that we STA W \ want to insert between the cars .rcar4 TXA \ Store X on the stack PHA LDA driversInOrder,X \ Set X to the number of the driver in position X TAX JSR MoveObjectBack \ Move driver X backwards along the track PLA \ Retrieve X from the stack TAX DEC W \ Decrement W BPL rcar4 \ Loop back until we have repeated the above W times INX \ Increment the loop counter CPX #20 \ Loop back until we have processed all 20 drivers BCC rcar3 .rcar5 \ This is where we join the loop with G = -1, so the \ following increments G to 0 as soon as we join \ \ This outer part of the loop runs through each position \ in G, from 0 to 19, and calls the inner loop above for \ each value of G INC G \ Increment G LDX G \ Set X = G, so the inner loop does G to 19 CPX #20 \ Loop back until we have done G = 0 to 19 BCC rcar3 \ At this point the cars are spaced out by the correct \ distance, working backwards from position 0 at the end \ of the track (i.e. just before the starting line), to \ position 19 at the back of the pack \ We now use the currently unused object 23 to work out \ the number of track segments we need to initialise in \ front of the current driver, by first moving forwards \ until we are exactly 32 segments in front of the \ current player, then moving backwards by 49 segments, \ and then moving backwards until we reach the start of \ the track section, leaving segmentCounter set to the \ total number of segments we have moved, starting from \ the start position of 32 segments in front of the \ current driver .rcar6 LDX #23 \ Set X to object 23 JSR MoveObjectForward \ Move object 23 forwards by one segment LDY #23 \ Set Y to object 23 LDX currentPlayer \ Set X to the driver number of the current player SEC \ Set the C flag for a 16-bit calculation in the call \ to CompareSegments JSR CompareSegments \ Set A and T to the distance between drivers X and Y BCS rcar6 \ If the C flag is set then the cars are far apart, so \ jump to rcar6 to keep moving object 23 forwards CMP #32 \ If A <> 32, jump to rcar6 to keep moving object 23 BNE rcar6 \ forwards \ At this point, object 23 is a distance of exactly 32 \ segments in front of the current player \ We now move object 23 back by 49 segments LDX #23 \ Set X to object 23 LDA #49 \ Set V = 49, to use as a loop counter from 49 to 1 STA V STA segmentCounter \ Set segmentCounter = 49 .rcar7 JSR MoveObjectBack \ Move object 23 backwards along the track DEC V \ Decrement the loop counter BNE rcar7 \ Loop back until we have moved object 23 backwards by \ 49 segments \ We now move object 23 backwards until it moves into a \ new track section, incrementing segmentCounter for \ each segment moved (which we set to 49 above, so it \ will keep tally of the total number of segments we \ have moved backwards) .rcar8 INC segmentCounter \ Increment segmentCounter JSR MoveObjectBack \ Move object 23 backwards along the track, setting the \ C flag if we move into a new track section BCC rcar8 \ Loop back to keep moving object 23 backwards until it \ moves into a new track section \ \ We don't care where object 23 has ended up, but we \ will use the value of segmentCounter below when \ populating the segment buffer \ We now move the cars to alternating sides of the track \ so the grid is staggered LDA #80 \ Set A to 80, which we will flip between 80 and 175 to \ alternate cars between the right (80) and left (175) \ side of the track, where 0 is full right and 255 is \ full left LDY #19 \ Set a loop counter in Y to loop through the positions .rcar9 LDX driversInOrder,Y \ Set X to the number of the driver in position X EOR #&FF \ Flip A between 80 and 175 STA carRacingLine,X \ Set the racing line of the car on the track (i.e. its \ left-right position) to the value in A DEY \ Decrement the loop counter to move on to the next \ position BPL rcar9 \ Loop back until we have staggered the whole pack LDA #0 \ Set frontSegmentIndex = 0, as the segment buffer is STA frontSegmentIndex \ currently empty (and we are about to fill it, which \ will update frontSegmentIndex accordingly) \ We now call GetTrackSegment segmentCounter times, \ where segmentCounter is the number of times we moved \ object 23 backwards in the above \ \ This populates the track segment buffer, making sure \ it is fully populated by working forwards from where \ object 23 ended up, one segment at a time, all the way \ to 32 segments in front of the current player, which \ is where we want the front segment of the track \ segment buffer to be .rcar10 JSR GetTrackSegment \ Initialise the next track segment in the track segment \ buffer, setting up the track segment indexes \ accordingly DEC segmentCounter \ Decrement the counter in segmentCounter BNE rcar10 \ Loop back until we have called GetTrackSegment \ segmentCounter times, by which time the buffer entry \ at frontSegmentIndex is 32 segments in front of the \ current player, the buffer entry at playerSegmentIndex \ is the current player, and all 40 entries in the \ buffer are populated with the correct segments \ \ So from this point onwards, object 23 is the object \ that corresponds to the front segment of the track \ segment buffer LSR updateLapTimes \ Clear bit 7 of updateLapTimes, so any further calls to \ MoveObjectForward will update the lap number and lap \ time once again RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: CheckForCrash \ Type: Subroutine \ Category: Car geometry \ Summary: Check to see if we have crashed into the fence, and if so, display \ the fence and make the crash sound \ \ ****************************************************************************** .CheckForCrash LDA edgeDistanceHi \ If edgeDistanceHi < 2, then we are too close to the CMP #2 \ verge to have reached the fence or to spin out on the BCC cras3 \ grass, so jump to cras3 to return from the subroutine \ If we get here then we are quite far off the track LDA edgeYawAngle \ Set A to the yaw angle of the track segment that is \ closest to the player's car, from the point of view of \ the car JSR Absolute8Bit \ Set A = |A| \ = |edgeYawAngle| CMP #96 \ If A >= 96, then the nearest track segment to the BCS cras1 \ player is within a cone stretching out behind the car \ to 45 degrees on each side, so that's between -96 and \ +96 in the following diagram: \ \ 0 \ -32 | +32 Overhead view of car \ \ | / \ \ | / 0 = looking straight ahead \ \|/ +64 = looking sharp right \ -64 -----+----- +64 -64 = looking sharp left \ /|\ \ / | \ \ / | \ \ -96 | +96 \ 128 \ \ If the nearest track segment is behind us in this way, \ jump to cras1 to crash into the fence \ If we get here then we have not hit the fence but we \ are off the track, so we spin out LDA #20 \ Set A = 20 so we apply a yaw angle spin of magnitude \ 20 to the car BIT spinYawAngleTop \ Set the N flag according to the sign in bit 7 of \ spinYawAngleTop, so the call to Absolute8Bit sets the \ sign of A to the same sign as the spin yaw angle (so \ the car spins out in the same direction as it is \ currently spinning) JSR Absolute8Bit \ Set A = 20 * abs(spinYawAngle) JMP SquealTyres \ Jump to SquealTyres to update spinYawAngle and make \ the tyres squeal, returning from the subroutine using \ a tail call .cras1 DEC crashedIntoFence \ Decrement crashedIntoFence from 0 to &FF so the main \ driving loop will pause while showing the fence INC horizonLine \ Increment horizonLine to simulate us ditching forward \ into the fence (so the horizon goes up by a line) JSR DrawFence \ Draw the fence that we crash into when running off the \ track JSR FlushSoundBuffers \ Flush all four sound channel buffers LDA #4 \ Make sound #4 (crash/contact) at the current volume JSR MakeSound-3 \ level LDA #0 \ Set A = 0, so we can use it to reset variables to zero \ in the following loop LDX #30 \ We now zero all 30 variable bytes from xPlayerSpeedHi \ to xSteeringForceHi, so set up a loop counter in X .cras2 STA xPlayerSpeedHi,X \ Zero the X-th byte from xPlayerSpeedHi DEX \ Decrement the loop counter BPL cras2 \ Loop back until we have zeroed all variables from \ xPlayerSpeedHi to xSteeringForceHi STA engineStatus \ Set engineStatus = 0 to turn off the engine STA yGravityDelta \ Set yGravityDelta = 0 to cancel the effect of gravity \ on the car STA soundRevCount \ Set soundRevCount = 0 to stop the engine sound STA soundRevTarget \ Set soundRevTarget = 0 to stop the engine sound LDA #127 \ Set heightAboveTrack = 127 so the car is dropped from STA heightAboveTrack \ this height by the crane (if this is a Novice race, in \ which case it restarts without calling ResetVariables, \ which otherwise would zero heightAboveTrack) LDA #31 \ Set oddsOfEngineStart = 31 to make it harder to STA oddsOfEngineStart \ restart the engine (with a 1 in 32 chance) .cras3 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: FinishRace \ Type: Subroutine \ Category: Main loop \ Summary: Continue running the race until all the non-player drivers have \ finished and we have a result \ \ ------------------------------------------------------------------------------ \ \ This routine is beavering away in the background when the game displays the \ "PLEASE WAIT" message, after we finish qualifying or racing. \ \ It keeps running the simulation in the background until all the other drivers \ have finished their race or qualifying laps. \ \ ****************************************************************************** .FinishRace LDA #0 \ Set playerMoving = 0 to denote that the player's car STA playerMoving \ is stationary STA raceStarting \ Set raceStarting = 0 so the call to MoveCars below \ will move the cars round the track JSR HideAllCars \ Set all the cars to be hidden LDX currentPlayer \ Clear the current player's total race time if this is JSR ClearTotalRaceTime \ an incomplete race .fini1 JSR ProcessTime \ Increment the timers and the main loop counter, and \ set the speed for the next non-player driver LDY #0 \ Check for SHIFT and right arrow JSR ProcessShiftedKeys LDA configStop \ If bit 7 of configStop is set then we must be pressing BMI fini4 \ either SHIFT-f0 for a pit stop or SHIFT and right \ arrow to restart the game, so jump to fini4 to return \ from the subroutine \ If we get here then the race or qualifying lap has \ finished naturally, rather than being aborted, and \ this is not a pit stop \ \ So we now need to keep running the race or qualifying \ lap (albeit with everything hidden from the player) to \ get the final set of results for the whole pack JSR MoveCars \ Move the cars around the track, to keep the race going JSR ProcessOvertaking \ Process overtaking manoeuvres for the non-player \ drivers JSR SetPlayerPositions \ Set the current player's position, plus the position \ ahead and the position behind LDX #19 \ We now loop through the drivers, checking whether they \ have finished the race, so set a loop counter in X for \ the driver number LDA raceStarted \ If bit 7 of raceStarted is set then this is a race BMI fini2 \ rather than practice or qualifying, so jump to fini2 \ to work through the drivers and wait for them all to \ finish the race \ If we get here, then this is a qualifying lap CPX currentPlayer \ If the player is not driver 19, then jump to fini4 to BNE fini4 \ return from the subroutine, as there is more than one \ human player and this is not the first human to race, \ so we already have qualifying times for all the \ drivers \ If we get here, then this is a qualifying lap and the \ current player is driver 19, which means this is \ either the only human player, or the first human to \ race \ \ In any event, we need to make sure that we run the \ qualifying lap long enough to get lap times for all \ the other drivers, so we run the race for at least \ 14 * 256 main loop iterations \ \ This ensures we get lap times for all the drivers, \ even if the player decides to quit qualifying early LDA mainLoopCounterHi \ If the high byte main loop counter < 14, loop back to CMP #14 \ fini1 to keep running the race BCC fini1 RTS \ Return from the subroutine .fini2 \ If we get here, then this is a race rather than \ qualifying LDA objectStatus,X \ If bit 6 of driver X's objectStatus is set, then AND #%01000000 \ driver X has finished the race, so jump to fini3 BNE fini3 \ to check the next driver LDA numberOfLaps \ If numberOfLaps >= driver X's lap number, jump to CMP driverLapNumber,X \ fini1 to keep running the race, as driver X hasn't BCS fini1 \ finished the race yet .fini3 DEX \ Decrement the driver counter in X BPL fini2 \ Loop back until all the drivers have finished the race .fini4 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: PushCarOffTrack \ Type: Subroutine \ Category: Car geometry \ Summary: Push a car off the track and out of the race \ Deep dive: Tactics of the non-player drivers \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The driver number of the car to push off the track \ \ ****************************************************************************** .PushCarOffTrack CPX #20 \ If X >= 20 then this is not a valid driver, so return BCS BuildPlayerCar-1 \ from the subroutine (as BuildPlayerCar-1 contains an \ RTS) LDA carRacingLine,X \ Fetch the car's current racing line AND #%01111111 \ Clear bit 7 and set bits 0, 2 and 6 ORA #%01000101 STA carSteering,X \ Update the car's steering byte, so the car does the \ following: \ \ * Bit 7 clear = veer to the left \ \ * Bit 6 set = only apply this steering if there is \ room to do so, otherwise just keep the \ car on the track \ \ * Bits 0-5 = set steering amount to at least 5 \ (%101) LDA #%10010001 \ Set bits 0, 4 and 7 of the car's status byte, so: STA carStatus,X \ \ * Bit 0 set = do not update this carStatus byte when \ in the ProcessOvertaking routine, so \ overtaking tactics are disabled \ \ * Bit 4 set = do not follow the segment's steering \ line in segmentSteering, so the car \ doesn't automatically steer around \ corners \ \ * Bit 7 set = apply brakes \ Fall through into ClearTotalRaceTime to set the car \ object to be hidden and no longer racing, and reset \ the car's total race time \ ****************************************************************************** \ \ Name: ClearTotalRaceTime \ Type: Subroutine \ Category: Drivers \ Summary: Clear a specified driver's total race time following the end of an \ incomplete race \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The driver number \ \ ****************************************************************************** .ClearTotalRaceTime LDA numberOfLaps \ Compare numberOfLaps with the driver X's lap number CMP driverLapNumber,X LDA #%11000000 \ Set bits 6 and 7 of the status byte for driver X's car STA objectStatus,X \ object, so it's hidden (bit 7) and is no longer racing \ (bit 6) BCC clap1 \ If numberOfLaps < driver X's lap number, then the \ driver has finished the race, so skip the following \ instruction STA totalRaceMinutes,X \ The player didn't finish the race, so set the player's \ total race time to &C0 minutes, or 120 minutes .clap1 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: BuildPlayerCar \ Type: Subroutine \ Category: 3D objects \ Summary: Build the objects for the player's car \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ xPlayerCoord The 3D coordinates of the player's car \ \ playerYawAngle The yaw angle of the player's car around the y-axis \ \ ------------------------------------------------------------------------------ \ \ Other entry points: \ \ BuildPlayerCar-1 Contains an RTS \ \ ****************************************************************************** .BuildPlayerCar LDX currentPlayer \ Set X to the driver number of the current player STX thisDriver \ Set thisDriver to the driver number of the current \ player STX objectNumber \ Set objectNumber to the driver number of the current \ player LDY playerSegmentIndex \ Build the car object for the current player using JSR BuildCarObjects \ the player's track segment from the track segment \ buffer, returning the car's 3D coordinates in xCoord2 LDX #2 \ We are about to copy the three axes of the resulting \ vectors, so set an axis counter in X .bpla1 LDA xCoord2Lo,X \ Copy the car's 3D coordinates from xCoord2 into STA xPlayerCoordHi,X \ xPlayerCoord LDA xCoord2Hi,X STA xPlayerCoordTop,X DEX \ Decrement the axis counter BPL bpla1 \ Loop back until we have copied all three axes LDA playerSegmentIndex \ Set A = playerSegmentIndex + 3 CLC \ ADC #3 \ to move on to the next track segment CMP #120 \ If A < 120, then we haven't reached the end of the BCC bpla2 \ track segment buffer, so jump to bpla2 to store the \ updated value LDA #0 \ We just reached the end of the track segment buffer, \ so set A = 0 to wrap round to the start .bpla2 TAY \ Set Y to the updated track segment index * 3 LDX thisDriver \ Set X to the driver number of the current player JSR BuildCarObjects \ Build the car object for the current player using the \ updated track segment, returning the object's yaw \ angle in objYawAngle LDA objYawAngleLo,X \ Copy the low byte of the yaw angle to playerYawAngleLo STA playerYawAngleLo LDA objYawAngleHi,X \ Copy the high byte of the yaw angle to EOR directionFacing \ playerYawAngleHi, flipping the sign of the STA playerYawAngleHi \ coordinate if we are facing backwards along the track RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: GetSectionCoord \ Type: Subroutine \ Category: Track geometry \ Summary: Copy a three-part 16-bit coordinate from the track section data \ Deep dive: Building a 3D track from sections and segments \ \ ------------------------------------------------------------------------------ \ \ This routine is normally called with X as a multiple of 3 in the range 0 to \ 117, representing track segments 0 to 39. The routine copies the following \ section coordinates from the track section data for section Y * 8: \ \ * xTrackSectionI \ * yTrackSectionI \ * zTrackSectionI \ \ and stores them in the X-th coordinate in (xSegmentCoordI, ySegmentCoordI, \ zSegmentCoordI). \ \ Specifically, this copies data from the Y-th track section entry: \ \ (xTrackSectionIHi xTrackSectionILo) \ (yTrackSectionIHi yTrackSectionILo) \ (zTrackSectionIHi zTrackSectionILo) \ \ and stores it in the X-th track segment: \ \ (xSegmentCoordIHi xSegmentCoordILo) \ (ySegmentCoordIHi ySegmentCoordILo) \ (zSegmentCoordIHi zSegmentCoordILo) \ \ This routine is also called with X = &FD, in which case it copies the \ following: \ \ Y-th (xTrackSectionIHi xTrackSectionILo) to (xCoord2Hi xCoord2Lo) \ Y-th (yTrackSectionIHi yTrackSectionILo) to (yCoord2Hi yCoord2Lo) \ Y-th (zTrackSectionIHi zTrackSectionILo) to (zCoord2Hi zCoord2Lo) \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ Y The number of the track section * 8 whose coordinates we \ want to fetch \ \ X The place to store the data: \ \ * 0-117 = The index * 3 of the track segment where we \ store the coordinates \ \ * &FD = Copy to (xCoord2, yCoord2, zCoord2) \ \ ****************************************************************************** .GetSectionCoord LDA xTrackSectionILo,Y \ Copy the following 16-bit coordinates: STA xSegmentCoordILo,X \ LDA yTrackSectionILo,Y \ * The Y-th xTrackSectionI to the X-th xSegmentCoordI STA ySegmentCoordILo,X \ LDA zTrackSectionILo,Y \ * The Y-th yTrackSectionI to the X-th ySegmentCoordI STA zSegmentCoordILo,X \ \ * The Y-th zTrackSectionI to the X-th zSegmentCoordI \ \ starting with the low bytes LDA xTrackSectionIHi,Y \ And then the high bytes STA xSegmentCoordIHi,X LDA yTrackSectionIHi,Y STA ySegmentCoordIHi,X LDA zTrackSectionIHi,Y STA zSegmentCoordIHi,X RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: GetSectionCoords \ Type: Subroutine \ Category: Track geometry \ Summary: Copy two three-part 16-bit coordinates from the track section data \ Deep dive: Building a 3D track from sections and segments \ \ ------------------------------------------------------------------------------ \ \ This routine is normally called with X as a multiple of 3 in the range 0 to \ 117, representing track segments 0 to 39. The routine copies the following \ track section coordinates from the track section data for section Y * 8: \ \ * xTrackSectionI \ * yTrackSectionI \ * zTrackSectionI \ \ and stores them in the X-th coordinate in (xSegmentCoordI, ySegmentCoordI, \ zSegmentCoordI). It also copies the following track section coordinates: \ \ * xTrackSectionO \ * yTrackSectionI \ * zTrackSectionO \ \ and stores them in the X-th coordinate in (xSegmentCoordO, ySegmentCoordO, \ zSegmentCoordO). Note that the y-coordinate is set to the same value as the \ y-coordinate from the first copy. \ \ It also sets thisVectorNumber to trackSectionFrom for the Y-th track section \ data. \ \ Specifically, this copies data from the Y-th track section entry: \ \ (xTrackSectionIHi xTrackSectionILo) \ (yTrackSectionIHi yTrackSectionILo) \ (zTrackSectionIHi zTrackSectionILo) \ (xTrackSectionOHi xTrackSectionOLo) \ (yTrackSectionIHi yTrackSectionILo) \ (zTrackSectionOHi zTrackSectionOLo) \ trackSectionFrom \ \ and stores it in the X-th track segment: \ \ (xSegmentCoordIHi xSegmentCoordILo) \ (ySegmentCoordIHi ySegmentCoordILo) \ (zSegmentCoordIHi zSegmentCoordILo) \ (xSegmentCoordOHi xSegmentCoordOLo) \ (ySegmentCoordOHi ySegmentCoordOLo) \ (zSegmentCoordOHi zSegmentCoordOLo) \ thisVectorNumber \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ Y The number of the track section * 8 whose coordinates we \ want to fetch \ \ X The index * 3 of the track segment where we store the \ coordinates \ \ ****************************************************************************** .GetSectionCoords JSR GetSectionCoord \ Copy the following 16-bit coordinate: \ \ * The Y-th xTrackSectionI to the X-th xSegmentCoordI \ \ * The Y-th yTrackSectionI to the X-th ySegmentCoordI \ \ * The Y-th zTrackSectionI to the X-th zSegmentCoordI LDA xTrackSectionOLo,Y \ Copy the following 16-bit coordinate: STA xSegmentCoordOLo,X \ LDA zTrackSectionOLo,Y \ * The Y-th xTrackSectionO to the X-th xSegmentCoordO STA zSegmentCoordOLo,X \ \ * The Y-th zTrackSectionO to the X-th zSegmentCoordO \ \ starting with the low bytes LDA xTrackSectionOHi,Y \ And then the high bytes STA xSegmentCoordOHi,X LDA zTrackSectionOHi,Y STA zSegmentCoordOHi,X LDA trackSectionFrom,Y \ Set thisVectorNumber = the Y-th trackSectionFrom STA thisVectorNumber \ Fall through into CopySectionData to copy the \ following 16-bit coordinate: \ \ * The Y-th yTrackSectionI to the X-th ySegmentCoordO \ \ This works because the call to GetSectionCoord already \ stored the Y-th yTrackSectionI in the X-th \ ySegmentCoordI, and the following now copies that into \ the X-th ySegmentCoordO \ ****************************************************************************** \ \ Name: CopySectionData \ Type: Subroutine \ Category: Track geometry \ Summary: Copy a 16-bit y-coordinate from the track section data \ Deep dive: Building a 3D track from sections and segments \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X Copy X-th (ySegmentCoordIHi ySegmentCoordILo) to \ (ySegmentCoordOHi ySegmentCoordOLo) \ \ ****************************************************************************** .CopySectionData LDA ySegmentCoordILo,X \ Copy the following 16-bit coordinate: STA ySegmentCoordOLo,X \ \ * The X-th ySegmentCoordI to the X-th ySegmentCoordO \ \ starting with the low byte LDA ySegmentCoordIHi,X \ And then the high byte STA ySegmentCoordOHi,X RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: GetPlayerIndex \ Type: Subroutine \ Category: Track geometry \ Summary: Set the index for the player's segment in the track section buffer \ to be 32 segments behind the front segment \ Deep dive: Data structures for the track calculations \ \ ****************************************************************************** .GetPlayerIndex LDA frontSegmentIndex \ Set A = the front track segment index * 3 - 96 SEC \ SBC #96 \ So this is 32 segments before the front track segment \ in the track segment buffer, as each buffer entry has \ three bytes BPL zsta1 \ If A < 0, set A = A + 120, so the index number wraps CLC \ around to the end of the stack ADC #120 .zsta1 STA playerSegmentIndex \ Set playerSegmentIndex to the updated index RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: GetFirstSegment \ Type: Subroutine \ Category: Track geometry \ Summary: Get the track section coordinates and flags from the track data \ and populate the first track segment \ Deep dive: Data structures for the track calculations \ \ ------------------------------------------------------------------------------ \ \ This routine is called when we move into a new track section, and is used to \ populate the front track segment with the data from the start of the new \ track section. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ sectionBehind The number * 8 of the track section behind us, for when \ we are facing backwards \ \ frontSegmentIndex The index * 3 of the new front track segment in the \ track segment buffer \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ sectionListSize Bits 0-2 from trackSectionData, or 2 if we are facing \ backwards \ \ xSegmentCoordI Start coordinates for the inside of the track section, \ stored in the new front track segment \ \ xSegmentCoordO Start coordinates for the outside of the track section, \ stored in the new front track segment \ \ thisVectorNumber The vector number from the start of the track section \ \ thisSectionFlags The track section flags for the section containing the \ new front track segment \ \ segmentFlags Zeroed for the new front track segment \ \ ****************************************************************************** .GetFirstSegment LDX frontSegmentIndex \ Set X to the index * 3 of the front track segment in \ the track segment buffer LDY #6 \ Set Y = 6 STY newSectionFetched \ Set newSectionFetched to a non-zero value to indicate \ that we have just fetched a new section (as the \ GetFirstSegment routine is only called when the \ player's car enters a new section LDA resetSectionList \ If resetSectionList = 0, then we do not need to reset BEQ getf1 \ the track section list, so jump to getf1 to skip the \ following instruction \ If we get here then resetSectionList is non-zero, \ which means we need to reset the track section list STY sectionListValid \ Set sectionListValid = 6 to indicate that the track \ section list contains no valid entries, so the list \ gets regenerated over the coming iterations .getf1 LDA directionFacing \ If our car is facing backwards, jump to getf2 BMI getf2 LDY objTrackSection+23 \ Set Y to the number * 8 of the track section for the \ front segment of the track segment buffer JSR GetSectionCoords \ Copy the start coordinates for the track section \ into xSegmentCoordI and xSegmentCoordO, and set \ thisVectorNumber to trackSectionFrom LDA trackSectionData,Y \ Set A = trackSectionData for the track section, so \ bits 0-2 can be set as the value for sectionListSize \ below JMP getf3 \ Jump to getf3 to skip the following .getf2 LDY sectionBehind \ Set Y to the number * 8 of the track section in \ sectionBehind JSR GetSectionCoords \ Copy the start coordinates for the track section \ into xSegmentCoordI and xSegmentCoordO, and set \ thisVectorNumber to trackSectionFrom JSR UpdateVectorNumber \ Update thisVectorNumber to the next vector along the \ track in the direction we are facing LDA #2 \ Set A = 2, to use as the value for sectionListSize \ below .getf3 AND #%00000111 \ Set sectionListSize = bits 0-2 from A STA sectionListSize LDY objTrackSection+23 \ Set Y to the number * 8 of the track section for the \ new front segment LDA trackSectionFlag,Y \ Set thisSectionFlags to the track section flags for STA thisSectionFlags \ the section containing the new front track segment LDA #0 \ Zero the flags for the front track segment STA segmentFlags,X RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: ShuffleSectionList \ Type: Subroutine \ Category: Track geometry \ Summary: Shuffle the track section list along by one position \ Deep dive: Data structures for the track calculations \ \ ------------------------------------------------------------------------------ \ \ The track section list lives in the first six bytes of the following variable \ blocks, and contains data on a number of track sections that we use when \ calculating the positions of the track verges: \ \ * xVergeLeftLo \ * xVergeLeftHi \ * yVergeLeft \ * xVergeRightLo \ * xVergeRightHi \ * yVergeRight \ \ This routine shuffles the list along by one position, so we can insert a new \ track section into position 0 while pushing the data in position 5 off the \ end of the list. \ \ The remaining bytes in the blocks are used to store track segment data. \ \ ****************************************************************************** .ShuffleSectionList LDX #44 \ Set X = 44 so we start by shuffling the first batch: \ \ * xVergeLeftLo+0-4 to xVergeLeftLo+1-5 \ \ * xVergeLeftHi+0-4 to xVergeLeftHi+1-5 \ \ * yVergeLeft+0-4 to yVergeLeft+1-5 \ \ This works because: \ \ * xVergeRightLo + 40 = xVergeLeftLo \ \ * xVergeRightHi + 40 = xVergeLeftHi \ \ * yVergeRight + 40 = yVergeLeft .shuf1 LDA xVergeRightLo,X \ Shuffle the X-th byte of xVergeRightLo up by one STA xVergeRightLo+1,X LDA xVergeRightHi,X \ Shuffle the X-th byte of xVergeRightHi up by one STA xVergeRightHi+1,X LDA yVergeRight,X \ Shuffle the X-th byte of yVergeRight up by one STA yVergeRight+1,X CPX #40 \ If X <> 40 then we are either still shuffling the BNE shuf2 \ first batch and haven't yet done the last shuffle, or \ we are already shuffling the second batch, so in \ either case jump to shuf2 to skip the following LDX #5 \ We have just done the last shuffle in the first batch, \ so set X = 5 so we now shuffle the second batch: \ \ * xVergeRightLo+0-4 to xVergeRightLo+1-5 \ \ * xVergeRightHi+0-4 to xVergeRightHi+1-5 \ \ * yVergeRight+0-4 to yVergeRight+1-5 .shuf2 DEX \ Decrement the loop counter BPL shuf1 \ Loop back until we have shuffled all the bytes in both \ batches LDA #6 \ Set sectionListStart = 6 - sectionListSize SEC \ SBC sectionListSize \ so sectionListStart is the start index of the track STA sectionListStart \ section list, as the list ends at index 6 JSR IncSectionPointers \ Update the list pointer variables so the new entry is \ not marked as valid RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: IncSectionPointers \ Type: Subroutine \ Category: Track geometry \ Summary: Increment the track section list pointers following a shuffle \ Deep dive: Data structures for the track calculations \ \ ------------------------------------------------------------------------------ \ \ This routine increments sectionListValid and sectionListPointer so they move \ along with the newly shuffled track section list. \ \ ****************************************************************************** .IncSectionPointers LDX sectionListValid \ Set X = sectionListValid + 1 INX \ \ If the whole section list is valid, then this marks \ the first entry in the list as invalid, which we want \ to do as we just shuffled the list and inserted a \ dummy entry into the start of the list \ \ If the whole section is not valid, then this moves \ the valid pointer along with the shuffled entries CPX #6 \ If X < 6, then the new value of sectionListValid is BCC incp1 \ within the list, so jump to incp1 to skip the \ following LDX #6 \ Set X = 6, so the maximum value of sectionListValid \ is 6 .incp1 CPX sectionListStart \ If X >= sectionListStart, then the new value of BCS incp2 \ sectionListValid is within the list, so jump to incp2 \ to skip the following LDX sectionListStart \ Set X = sectionListStart, so the minimum value of \ sectionListValid is sectionListStart, at the start of \ the list .incp2 STX sectionListValid \ Store the updated value of X in sectionListValid, so \ we mark the new entry that we shuffled in as invalid LDX sectionListPointer \ Set X = sectionListPointer + 1 INX \ Fall through into SetSectionPointers to set the track \ section list pointer to the new value, i.e. increment \ the pointer so it moves along with the shuffled values \ ****************************************************************************** \ \ Name: SetSectionPointers \ Type: Subroutine \ Category: Track geometry \ Summary: Set the track section list pointer to a specific value and update \ the validity pointer accordingly \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The new value for sectionListPointer \ \ ****************************************************************************** .SetSectionPointers INX \ Set X = X + 1 \ \ So X points to the entry above the proposed pointer \ value in the list CPX sectionListValid \ If X >= sectionListValid, then this entry is valid, so BCS secp1 \ jump to secp1 to skip the following STX sectionListValid \ Set sectionListValid = X, so the value at the proposed \ pointer is flagged as not valid, but the value above \ it is valid .secp1 DEX \ Set X = X - 1 \ \ so X is back to its original value, the proposed value \ of sectionListPointer CPX sectionListStart \ If X >= sectionListStart, then the proposed pointer BCS secp2 \ value is not before the start of the list, so jump to \ secp2 to skip the following LDX #5 \ The proposed pointer is not within the list, so set \ X = 5, so we set sectionListPointer to 5 below .secp2 CPX #6 \ If X < 6, then the proposed pointer value is not past BCC secp3 \ the end of the list, so jump to secp3 to skip the \ following LDX #5 \ The proposed pointer is past the end of the list, so \ set X = 5, so we set sectionListPointer to 5 below .secp3 STX sectionListPointer \ Store the updated value of X in sectionListPointer RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: MovePlayerForward \ Type: Subroutine \ Category: Car geometry \ Summary: Move the player's car forwards by one track segment and add the \ segment to the track segment buffer \ \ ****************************************************************************** .MovePlayerForward CLC \ Clear the C flag so the call to MovePlayer moves the \ player's car in the direction it is pointing JSR MovePlayer \ Move the player's car forwards by one track segment \ Fall through into GetTrackSegment to set up the \ next track segment \ ****************************************************************************** \ \ Name: GetTrackSegment (Part 1 of 3) \ Type: Subroutine \ Category: Track geometry \ Summary: Set up the next track segment in the track segment buffer \ Deep dive: Building a 3D track from sections and segments \ Data structures for the track calculations \ Corner markers \ \ ------------------------------------------------------------------------------ \ \ This routine calculates the coordinates, flags and cornering data for the next \ track segment. It does this in three parts: \ \ 1. Initialise up the new track segment, and fetch the segment vector for the \ new track segment \ \ 2. Set the flags for the new track segment \ \ 3. Add the segment vector from part 1 to the coordinates for the current \ track segment to get the coordinates for the next track segment \ \ This part does the following: \ \ * Move the index on to the next track segment \ \ * Initialise the new track segment if we are entering a new track section \ \ * Set pastHalfway \ \ * Set (SS T), (TT U) and (UU V) to the xTrackSegmentI, yTrackSegmentI and \ zTrackSegmentI vectors \ \ ****************************************************************************** .GetTrackSegment LDA frontSegmentIndex \ Set A to the index * 3 of the front track segment in \ the track segment buffer STA prevSegmentIndex \ Store A in prevSegmentIndex, as we are about to move \ on to the next track segment CLC \ Set A = frontSegmentIndex + 3 ADC #3 \ \ to move on to the next track segment ahead of the \ current front segment in the track segment buffer, \ which will become the new front segment CMP #120 \ If A < 120, then we haven't reached the end of the BCC gets1 \ track segment buffer, so jump to gets1 to store the \ updated value LDA #0 \ We just reached the end of the track segment buffer, \ so set A = 0 to wrap round to the start .gets1 STA frontSegmentIndex \ Set frontSegmentIndex to the index * 3 of the new \ front track segment LDX #23 \ Set X to 23, the object number we use to store the \ front segment of the track segment buffer LDA directionFacing \ If our car is facing backwards, jump to gets3 BMI gets3 LDA trackSectionCount \ Set A to half the total number of track sections * 4 LSR A AND #%11111000 \ Reduce A to the nearest multiple of 8 CMP objTrackSection,X \ If A <> number of the track section * 8 for the front BNE gets2 \ segment, then we are not halfway through all the track \ sections, so jump to gets2 \ If we get here then the front segment is at the \ halfway point round the track in terms of track \ section numbers LDA #1 \ Set pastHalfway = 1, to denote that the front segment STA pastHalfway \ is in the second half of the track .gets2 JSR MoveObjectForward \ Move the front segment forwards by one segment, \ setting the C flag if this moves the driver into the \ next track section BCC gets4 \ If the front segment is still in the same track \ section, jump to gets4 JSR GetFirstSegment \ Otherwise we just moved into a new track section, so \ get the track section coordinates and flags from the \ track data and populate the first track segment, \ setting thisVectorNumber to the number of the segment \ vector for the new track segment JMP gets13 \ Jump to gets13 to finish setting up the track segment, \ skipping the part that sets the coordinates and flags, \ as those have just been set when creating the entry \ for the new track section .gets3 \ If we get here then our car is facing backwards LDA objTrackSection+23 \ Set sectionBehind to the number * 8 of the track STA sectionBehind \ section for the front segment JSR MoveObjectBack \ Move the front segment backwards along the track, \ setting the C flag if this moves the driver into the \ next track section BCC gets4 \ If the front segment is still in the same track \ section, jump to gets4 JSR GetFirstSegment \ Otherwise we just moved into a new track section, so \ get the track section coordinates and flags from the \ track data and populate the first track segment, \ setting thisVectorNumber to the number of the segment \ vector for the new track segment .gets4 LDY thisVectorNumber \ Set Y to the number of the segment vector for the new \ track segment JSR GetSegmentVector \ Set (SS T), (TT U) and (UU V) to the coordinates of \ the segment vector for the new track segment, so: \ \ [ (SS T) ] [ xTrackSegmentI ] \ [ (TT U) ] = [ yTrackSegmentI ] \ [ (UU V) ] [ zTrackSegmentI ] \ \ In other words, they contain the vector that we need \ to add to the previous track segment's coordinates in \ order to get the coordinates for the new track segment \ \ Or, even simpler, they contain the vector from the \ previous track segment's coordinates to the new one \ ****************************************************************************** \ \ Name: GetTrackSegment (Part 2 of 3) \ Type: Subroutine \ Category: Track geometry \ Summary: Set the flags for the new front segment in the track segment \ buffer \ Deep dive: Building a 3D track from sections and segments \ Data structures for the track calculations \ Corner markers \ \ ------------------------------------------------------------------------------ \ \ This part does the following: \ \ * Set the flags for the new front track segment in the track segment buffer \ \ Set the flags for the new track segment to the flags for the track section \ from the track data, but with these changes: \ \ Curve: \ \ * If the new front segment is not exactly halfway through the section, \ clear bits 3-5, to hide all corner markers (so they only get shown halfway \ through the section) \ \ Straight: \ \ * If the new front segment's segment number within the track section is not \ in the range 1 to 9, clear bits 1, 2 to show a black-and-white verge \ \ * If the distance yet to cover in this section = 7, leave bit 5 alone, so if \ red corner markers are configured, they are left as red \ \ * If the distance yet to cover in this section = 14 or 21, clear bit 5 to \ show all corner markers in white \ \ * If the distance yet to cover in this section <> 7, 14 or 21, clear bits \ 3-5, to hide all corner markers \ \ ****************************************************************************** LDX frontSegmentIndex \ Set X to the index * 3 of the front track segment in \ the track segment buffer LDA thisSectionFlags \ Store bit 0 of the current section's flags on the LSR A \ stack using the C flag PHP LDA thisSectionFlags \ Set A to the current section's flags BCS gets6 \ If bit 0 of the current section's flag byte is set, \ then this is a curved section, so jump to gets6 with \ A set to the current section's flags, so the new \ track segment retains the same flags \ If we get here then this is a straight track section LDY objSectionSegmt+23 \ Set Y to the new front segment's segment number within \ the track section CPY #1 \ If Y < 1, jump to gets5 BCC gets5 CPY #10 \ If Y < 10, jump to gets6 with A set to BCC gets6 \ thisSectionFlags .gets5 \ If we get here then Y < 1 or Y >= 10 AND #%11111001 \ Clear bits 1 and 2 of the current section's flags in A \ to give this segment a black-and-white verge .gets6 STA W \ Store A in W, so W contains: \ \ * The current section's flags if this is a curve, or \ if this is a straight and the new segment's number \ within the track section is in the range 1 to 9 \ \ * The current section's flags with bits 1 and 2 \ cleared otherwise (i.e. if this is a straight and \ the new segment's number within the track section \ is not in the range 1 to 9) \ \ We use W to build the flags for the new track segment LDY objTrackSection+23 \ Set Y to the number * 8 of the track section for the \ new front segment LDA trackSectionSize,Y \ Set A to the size of the track section for the new \ front segment PLP \ If bit 0 of the current section's flag byte is clear, BCC gets7 \ then this is a straight section, so jump to gets7 with \ A set to the size of the track section for the new \ front segment \ If we get here then this is a curved track section LSR A \ Set Y = A / 2 TAY \ \ So Y contains half the size of the curved section LDA W \ Set A = W, which contains the flags we are building \ for the new track segment CPY objSectionSegmt+23 \ If Y = the new front segment's number within the track BEQ gets10 \ section, then the driver is exactly halfway round the \ curve, so jump to gets10 to store A in the track \ segment's flags BNE gets8 \ Otherwise jump to gets8 to clear bits 3-5 of A before \ storing it in the track segment's flags .gets7 \ If we get here then this is a straight section and A \ is set to the size of the track section for the new \ front segment SEC \ Set Y = A - the new front segment's number within the SBC objSectionSegmt+23 \ track section TAY \ \ So Y contains the length of the track section between \ the new front segment and the end of the section LDA W \ Set A = W, which contains the flags we are building \ for the new track segment CPY #7 \ If Y = 7, jump to gets10 to store A in the track BEQ gets10 \ segment's flags CPY #14 \ If Y = 14 or 21, jump to gets9 to clear bit 5 of A BEQ gets9 \ and store it in the track segment's flags CPY #21 BEQ gets9 \ Otherwise we clear bits 3-5 of A and store it in the \ track segment's flags .gets8 AND #%11100111 \ Clear bits 3 and 4 of A, so we don't show any corner \ markers for this segment .gets9 AND #%11011111 \ Clear bit 5 of A, so any markers for this segment are \ shown in white .gets10 AND thisSectionFlags \ Clear any bits in A that are already clear in the \ current section's flags, to ensure that the track \ segment's flags only have flags set if they are set in \ the track section's flags STA segmentFlags,X \ Set the flags for the new track segment to A \ ****************************************************************************** \ \ Name: GetTrackSegment (Part 3 of 3) \ Type: Subroutine \ Category: Track geometry \ Summary: Set the inner and outer track coordinates for the new track \ segment \ Deep dive: Building a 3D track from sections and segments \ Data structures for the track calculations \ Corner markers \ \ ------------------------------------------------------------------------------ \ \ This part calculates the inner track section coordinates for the new track \ segment, as follows: \ \ [ xSegmentCoordI ] [ previous xSegmentCoordI ] [ xTrackSegmentI ] \ [ ySegmentCoordI ] = [ previous ySegmentCoordI ] + [ yTrackSegmentI ] \ [ zSegmentCoordI ] [ previous zSegmentCoordI ] [ zTrackSegmentI ] \ \ It then calculates the outer track section coordinates for the new track \ segment, as follows: \ \ [ xSegmentCoordO ] [ xSegmentCoordI ] [ xTrackSegmentO ] \ [ ySegmentCoordO ] = [ ySegmentCoordI ] + [ 0 ] * 4 \ [ zSegmentCoordO ] [ zSegmentCoordI ] [ zTrackSegmentO ] \ \ ****************************************************************************** LDY prevSegmentIndex \ Set Y to the index * 3 of the previous track segment JSR AddVectors \ Add the (SS T), (TT U) and (UU V) vectors to the \ inner track section coordinates for the previous \ track segment and store the result in the current \ track segment: \ \ [ (SS T) ] \ this entry = previous entry + [ (TT U) ] \ [ (UU V) ] \ \ This correctly sets the inner track coordinates for \ the new track segment, as we set the (SS T), (TT U) \ and (UU V) vectors at the end of part 1 to the vector \ from the previous track segment's coordinates to the \ new one \ \ In other words, this does the following: \ \ [ xSegmentCoordI ] [ previous xSegmentCoordI ] \ [ ySegmentCoordI ] = [ previous ySegmentCoordI ] \ [ zSegmentCoordI ] [ previous zSegmentCoordI ] \ \ [ xTrackSegmentI ] \ + [ yTrackSegmentI ] \ [ zTrackSegmentI ] JSR CopySectionData \ Copy the X-th ySegmentCoordI to ySegmentCoordO \ We now set the outer track coordinates for the new \ track segment, as follows: \ \ [ xSegmentCoordO ] [ xSegmentCoordI ] \ [ ySegmentCoordO ] = [ ySegmentCoordI ] \ [ zSegmentCoordO ] [ zSegmentCoordI ] \ \ [ xTrackSegmentO ] \ + [ 0 ] * 4 \ [ zTrackSegmentO ] \ \ Note that we don't have to do the y-coordinate \ addition, as ySegmentCoordO was already set to \ ySegmentCoordI when the track segment was set up LDY thisVectorNumber \ Set Y to the number of the segment vector for the new \ track segment LDA #0 \ Set SS = 0, to use as the high byte in (SS A) below STA SS STA UU \ Set UU = 0, to use as the high byte in (UU A) below LDA xTrackSegmentO,Y \ Set A = the 3D x-coordinate of the outer segment \ vector for the new track segment BPL gets11 \ If A is negative, decrement SS to &FF so (SS A) has DEC SS \ the correct sign .gets11 ASL A \ Set (SS A) = (SS A) * 4 ROL SS \ = xTrackSegmentO * 4 ASL A ROL SS CLC \ Add the inner track section x-coordinate and the outer ADC xSegmentCoordILo,X \ x-coordinate * 4 to get the outer x-coordinate for the STA xSegmentCoordOLo,X \ new track segment, starting with the low bytes LDA SS \ And then the high bytes ADC xSegmentCoordIHi,X STA xSegmentCoordOHi,X LDA zTrackSegmentO,Y \ Set A = the z-coordinate of the outer segment vector \ for the new track segment BPL gets12 \ If A is negative, decrement UU to &FF so (UU A) has DEC UU \ the correct sign .gets12 ASL A \ Set (UU A) = (UU A) * 4 ROL UU \ = zTrackSegmentO * 4 ASL A ROL UU CLC \ Add the inner track section z-coordinate and the outer ADC zSegmentCoordILo,X \ z-coordinate * 4 to get the outer z-coordinate for the STA zSegmentCoordOLo,X \ new track segment, starting with the low bytes LDA UU \ And then the high bytes ADC zSegmentCoordIHi,X STA zSegmentCoordOHi,X JSR UpdateCurveVector \ If this is a curved track section, update the value of \ thisVectorNumber to the next segment vector along the \ track in the direction we are facing .gets13 LDX frontSegmentIndex \ Set X to the index * 3 of the front track segment in \ the track segment buffer LDA thisVectorNumber \ Set segmentVector for the new track segment to the STA segmentVector,X \ number of the segment vector for the track section \ (which will be unchanged from the previous section if \ this is a straight, or the next segment vector if this \ is a curve) JSR GetPlayerIndex \ Update the index for the segment containing the player \ in the track segment buffer JSR GetSegmentSteering \ Set segmentSteering for the new track segment to the \ optimum steering to apply at this point in the section RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: UpdateCurveVector \ Type: Subroutine \ Category: Track geometry \ Summary: Move to the next segment vector along in the direction we are \ facing, but only for curved track sections \ Deep dive: Building a 3D track from sections and segments \ \ ****************************************************************************** .UpdateCurveVector LDA thisSectionFlags \ If bit 0 of the track section's flag byte is clear, AND #1 \ then this is a straight track section, so return from BEQ ChangeDirection-1 \ the subroutine (as ChangeDirection-1 contains an RTS) \ Otherwise this is a curved track section, so fall \ through into UpdateVectorNumber to update the value \ of thisVectorNumber to the number of the next vector \ along the track \ ****************************************************************************** \ \ Name: UpdateVectorNumber \ Type: Subroutine \ Category: Track geometry \ Summary: Move to the next segment vector along the track in the direction \ we are facing \ Deep dive: Building a 3D track from sections and segments \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ thisVectorNumber Set to the number of the next segment vector along the \ track in the direction in which we are facing \ \ ****************************************************************************** .UpdateVectorNumber LDA directionFacing \ If our car is facing backwards, jump to uvec1 BMI uvec1 LDY thisVectorNumber \ Set Y to the current segment vector number INY \ Increment Y to point to the next segment vector number \ along the track CPY trackVectorCount \ If Y <> trackVectorCount, then we have not reached the BNE uvec3 \ last vector, so jump to uvec3 to store the new value \ of thisVectorNumber LDY #0 \ If we get here then we have reached the last vector, \ so set Y = 0 to wrap around to the first vector BEQ uvec3 \ Jump to uvec3 to set thisVectorNumber = 0 (this BEQ is \ effectively a JMP as Y is always zero) .uvec1 \ If we get here then our car is facing backwards LDY thisVectorNumber \ Set Y to the current segment vector number BNE uvec2 \ If Y <> 0, then we are not on the first segment \ vector, so jump to uvec2 to decrement to the previous \ vector LDY trackVectorCount \ Set Y = trackVectorCount, so we wrap around to the \ last vector .uvec2 DEY \ Decrement Y to point to the previous segment vector \ number, i.e. backwards along the track .uvec3 STY thisVectorNumber \ Update thisVectorNumber with the new value that we set \ above .uvec4 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: ChangeDirection \ Type: Subroutine \ Category: Car geometry \ Summary: Update the track segment buffer when the player's car spins so it \ changes the direction in which it is facing along the track \ \ ------------------------------------------------------------------------------ \ \ Other entry points: \ \ ChangeDirection-1 Contains an RTS \ \ ****************************************************************************** .ChangeDirection LDA #6 \ Set sectionListValid = 6, to invalidate the contents STA sectionListValid \ of the track section list (so it gets repopulated \ by the GetTrackAndMarkers routine) LDX #64 \ Set X = 64, so the call to TurnPlayerAround \ initialises 64 track segments in the new direction STX debugSpinning \ Set debugSpinning = 64 (this value is never read and \ is not used anywhere, so perhaps it was used for \ debugging purposes, or was left over from code that \ was subsequently removed?) JSR TurnPlayerAround \ Turn the player around and initialise 64 track \ segments in the new direction LDA #0 \ Set debugSpinning = 0 (which has no effect) STA debugSpinning RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: MovePlayerBack \ Type: Subroutine \ Category: Car geometry \ Summary: Move the player's car backwards by one track segment and update \ the track segment buffer \ \ ------------------------------------------------------------------------------ \ \ This routine reverses the player, updating the track segment buffer in both \ directions. \ \ ****************************************************************************** .MovePlayerBack SEC \ Set the C flag so the call to MovePlayer moves the \ player's car in the opposite direction to which it is \ pointing JSR MovePlayer \ Drive the player's car backwards by one track segment LDX #40 \ Set X = 40, so the call to TurnPlayerAround \ initialises 64 track segments in the new direction STX resetSectionList \ Set resetSectionList to a non-zero value, so if the \ calls to TurnPlayerAround need to fetch load a new \ track section, then we reset the track section list JSR TurnPlayerAround \ Turn the player around and initialise 40 track \ segments in the new direction LDX #39 \ Set X = 39, so the call to TurnPlayerAround \ initialises 64 track segments in the new direction JSR TurnPlayerAround \ Turn the player back around and initialise 39 track \ segments in the new direction LDA #0 \ Set resetSectionList = 0, so future calls to the STA resetSectionList \ TurnPlayerAround routine will update the track section \ list as normal RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: TurnPlayerAround \ Type: Subroutine \ Category: Car geometry \ Summary: Turn the player around and initialise the specified number of \ track segments in the new direction \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The number of track segments to initialise in the new \ direction \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ directionFacing Bit 7 is flipped to point us in the opposite direction \ \ ****************************************************************************** .TurnPlayerAround LDA directionFacing \ Flip bit 7 of directionFacing to denote that our car EOR #%10000000 \ is facing in the other direction STA directionFacing JSR UpdateCurveVector \ If this is a curved track section, update the value of \ thisVectorNumber to the next segment vector along the \ track in the new direction we are facing STX segmentCounter \ We now want to initialise X track segments in the new \ direction, so set a loop counter in segmentCounter \ that starts from X and counts down .turn1 JSR GetTrackSegment \ Initialise the next track segment in the track segment \ buffer DEC segmentCounter \ Decrement the loop counter BNE turn1 \ Loop back until we have set all X track segments RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: MovePlayer \ Type: Subroutine \ Category: Car geometry \ Summary: Move the player's car forwards or backwards by one segment \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ C flag Controls the direction of movement: \ \ * Clear = move the car in the direction it is pointing \ (i.e. drive forwards) \ \ * Set = reverse the car \ \ ****************************************************************************** .MovePlayer LDX currentPlayer \ Set X to the driver number of the current player ROR A \ If just one of the C flag and bit 7 of directionFacing EOR directionFacing \ is set (but not both), jump to play1 to move the car BMI play1 \ backwards along the track \ \ In other words, we move backwards if: \ \ * C is clear and the car is facing backwards along \ the track, in which case it is driving forwards \ but going backwards around the track \ \ * C is set and the car is facing forwards along the \ track, in which case it is reversing along the \ track while facing forwards JSR MoveObjectForward \ Move the current player forwards by one segment RTS \ Return from the subroutine .play1 JSR MoveObjectBack \ Move current player backwards by one segment RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: GetSegmentVector \ Type: Subroutine \ Category: Track geometry \ Summary: Fetch a segment vector from the track data file \ Deep dive: Building a 3D track from sections and segments \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ Y The index of the coordinate in the track data blocks \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ (SS T) The Y-th entry from xTrackSegmentI as a 16-bit signed \ integer \ \ (TT U) The Y-th entry from yTrackSegmentI as a 16-bit signed \ integer \ \ (UU V) The Y-th entry from zTrackSegmentI as a 16-bit signed \ integer \ \ ****************************************************************************** .GetSegmentVector LDA #0 \ Zero the high bytes of (SS T), (TT U) and (UU V) STA SS STA TT STA UU LDA xTrackSegmentI,Y \ Set T = the Y-th entry from xTrackSegmentI STA T BPL coor1 \ If the byte we just fetched is negative, decrement DEC SS \ the high byte in SS to &FF, so (SS T) has the correct \ sign .coor1 LDA yTrackSegmentI,Y \ Set U = the Y-th entry from yTrackSegmentI STA U BPL coor2 \ If the byte we just fetched is negative, decrement DEC TT \ the high byte in TT to &FF, so (TT U) has the correct \ sign .coor2 LDA zTrackSegmentI,Y \ Set V = the Y-th entry from zTrackSegmentI STA V BPL coor3 \ If the byte we just fetched is negative, decrement DEC UU \ the high byte in UU to &FF, so (UU V) has the correct \ sign .coor3 LDA directionFacing \ If our car is facing forwards, jump to coor5 to return BEQ coor5 \ from the subroutine \ We are facing backwards, so we need to negate the \ coordinate we just fetched LDX #2 \ We are about to negate the following 16-bit variables: \ \ (SS T) \ (TT U) \ (UU V) \ \ so set a counter in X to use as an index that loops \ through 2, 1 and 0, as: \ \ (TT U) = (SS+1 U+1) \ (UU V) = (SS+2 U+2) \ \ The following comments are for (SS T), but the same \ process applies for (TT U) and (UU V) .coor4 LDA #0 \ Set (SS T) = 0 - (SS T) SEC \ SBC T,X \ starting with the low bytes STA T,X LDA #0 \ And then the high bytes SBC SS,X STA SS,X DEX \ Decrement the loop counter to move on to the next \ variable BPL coor4 \ Loop back until we have negated all three 16-bit \ variables .coor5 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: MoveObjectForward \ Type: Subroutine \ Category: 3D objects \ Summary: Move a specified object forwards along the track by one segment \ \ ------------------------------------------------------------------------------ \ \ Moves object X forwards by one segment, updating the section number if we \ cross into a new section. \ \ Also updates the lap number and lap time, but only if this is a driver and bit \ 7 of updateLapTimes is clear. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X Driver number (0-23) \ \ updateLapTimes If bit 7 is set, the call to UpdateLaps has no effect, \ so we do not update the lap number or lap time \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ C flag Whether the driver has moved into a new section: \ \ * Clear if the driver is still within the same track \ section as before \ \ * Set if the driver has now moved into the next track \ section \ \ ****************************************************************************** .MoveObjectForward LDY objTrackSection,X \ Set Y to the track section number * 8 for object X LDA objSectionSegmt,X \ Set A = objSectionSegmt + 1 CLC \ ADC #1 \ This increments the section segment counter, which \ keeps track of the object's segment number in the \ current track section, so it moves forward one segment CMP trackSectionSize,Y \ If A < Y-th trackSectionSize, then the object is still PHP \ within the current track section, so clear the C flag \ and store it on the stack, otherwise the object has \ now reached the end of the current section, so set the \ C flag and store it on the stack BCC fore2 \ If A < Y-th trackSectionSize, then the object is still \ within the current track section, so jump to fore2 to \ update the track section counter with the new value TYA \ Set A = Y + 8 CLC \ ADC #8 \ So A contains the track section number * 8 of the next \ track section CMP trackSectionCount \ If A < trackSectionCount then this isn't the last BCC fore1 \ section before we wrap round to zero again, so jump to \ fore1 to skip the following instruction LDA #0 \ Set A = 0, so the track section number wraps round to \ zero when we reach the last section .fore1 STA objTrackSection,X \ Update the object's track section number to the new \ one that they just moved into LDA #0 \ The object just entered a new track section, so we \ need to zero the section counter, which keeps track of \ how far through the current section the object is, so \ set A to 0 to set as its new value below .fore2 STA objSectionSegmt,X \ Set the track section counter to the new value \ We now need to increment the object's segment number \ in (objectSegmentHi objectSegmentLo) INC objectSegmentLo,X \ Increment (objectSegmentHi objectSegmentLo) for object \ X, starting with the low byte BNE fore3 \ And then the high byte, if the low byte overflows INC objectSegmentHi,X .fore3 LDA objectSegmentLo,X \ If objectSegment <> trackLength, then the object has CMP trackLength \ not yet reached the end of the track, so jump to fore4 BNE fore4 \ to return from the subroutine as we are done LDA objectSegmentHi,X CMP trackLength+1 BNE fore4 LDA #0 \ The object has just reached the end of the track, so STA objectSegmentLo,X \ set (objectSegmentHi objectSegmentLo) = 0 to wrap STA objectSegmentHi,X \ round to the start again JSR UpdateLaps \ Increment the lap number and lap times for object X, \ for when this object is a car .fore4 PLP \ Retrieve the C flag from the stack so we can return it \ from the subroutine, so it is clear if the driver is \ still within the same track section, set otherwise RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: MoveObjectBack \ Type: Subroutine \ Category: 3D objects \ Summary: Move a specified object backwards along the track by one segment \ \ ------------------------------------------------------------------------------ \ \ Moves object X backwards by one segment, updating the section number if we \ cross into a new section. \ \ Also updates the lap number if this is the current player. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X Object number (0-23) \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ C flag Whether the object has moved into a new section: \ \ * Clear if the object is still within the same track \ section as before \ \ * Set if the object has now moved back into the \ previous track section \ \ ****************************************************************************** .MoveObjectBack LDY objTrackSection,X \ Set Y to the track section number * 8 for object X LDA objSectionSegmt,X \ Set A = objSectionSegmt, which keeps track of the \ object's segment number in the current track section CLC \ If A is non-zero then the object can move backwards by BNE back2 \ one step while staying in the same track section, so \ clear the C flag and jump to back2 to store this \ result on the stack \ Otherwise moving backwards will move the object into \ the previous track section TYA \ Store the current track section number * 8 in A BNE back1 \ If the track section number is non-zero, then skip the \ following instruction \ If we get here then the object is in the first track \ section and is moving backwards into the previous \ track section, so we have to wrap around to the end \ of the track to move into the very last track section LDA trackSectionCount \ Set A = trackSectionCount \ \ So A contains the number * 8 of the last track section \ plus 1 (as the track sections count from zero) .back1 \ By this point, A contains the number * 8 of the \ current track section, updated to cater for wrapping \ round at the end SEC \ Set A = A - 8 SBC #8 \ \ So A is now the number * 8 of the previous track \ section, which is where the driver is moving to STA objTrackSection,X \ Update the object's track section number to the new \ one that they just moved into TAY \ Set A to the size of the new track section, so A LDA trackSectionSize,Y \ contains the track section counter for the end of the \ new track section (so the object moves to the end of \ the new track section) SEC \ Set the C flag to indicate that the object has moved \ into a different track section .back2 PHP \ Store the C flag on the stack, which will be clear if \ the object is still within the same track section, set \ otherwise SEC \ A contains the object's segment number in the current SBC #1 \ track section, so subtract 1 to move the object \ backwards STA objSectionSegmt,X \ Set the track section counter to the new value .back3 \ We now need to decrement the object's segment number \ in (objectSegmentHi objectSegmentLo) LDA objectSegmentLo,X \ If the low byte of objectSegment for object X is BNE back4 \ non-zero, then jump to back4 to decrement the low \ byte, and we are done DEC objectSegmentHi,X \ Otherwise decrement the high byte BPL back4 \ If the high byte is positive, jump to back4 to \ decrement the low byte to &FF, and we are done \ If we get here then we just decremented the 16-bit \ value in (objectSegmentHi objectSegmentLo) into \ negative territory, so the object just moved \ backwards across the starting line, into the previous \ lap LDA trackLength \ Set objectSegment for driver X = trackLength STA objectSegmentLo,X \ LDA trackLength+1 \ So objectSegment wraps around to the segment number STA objectSegmentHi,X \ for the end of the track, as trackLength contains the \ length of the full track (in terms of segments) CPX currentPlayer \ If object X is not the current player, jump to back3 BNE back3 \ to decrement the newly wrapped segment number LDA driverLapNumber,X \ If the current lap number for the current player is BEQ back3 \ zero (i.e. this is the first lap) then jump to back3 \ to decrement the newly wrapped segment number DEC driverLapNumber,X \ Otherwise this is not the current player's first lap, \ and they just moved backwards, across the starting \ line and into the previous lap, so decrement the \ current lap number for the current player JMP back3 \ Jump to back3 to decrement the newly wrapped segment \ number .back4 DEC objectSegmentLo,X \ Decrement the low byte of the object's segment number \ to move it backwards PLP \ Retrieve the C flag from the stack so we can return it \ from the subroutine, so it is clear if the object is \ still within the same track section, set otherwise RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: GetSegmentSteering \ Type: Subroutine \ Category: Tactics \ Summary: Calculate the optimum steering to take for the current track \ segment \ Deep dive: Tactics of the non-player drivers \ \ ------------------------------------------------------------------------------ \ \ This routine populates segmentSteering for a track segment, depending on a \ large number of factors. \ \ ****************************************************************************** .GetSegmentSteering LDY objTrackSection+23 \ Set Y to the number * 8 of the track section for the \ front segment of the track segment buffer TYA \ Set X = Y / 8 LSR A \ LSR A \ So X contains the number of the track section for the LSR A \ front segment TAX LDA turnCounter \ If turnCounter is non-zero, then a turn is already in BNE rlin5 \ progress, so jump to rlin5 \ We get here if turnCounter is zero LDA thisSectionFlags \ If bit 0 of thisSectionFlags is set then this is a LSR A \ curved track section, so jump to rlin2 BCS rlin2 LDA objSectionSegmt+23 \ If the front segment's number within the track section CMP trackSectionTurn,Y \ is >= the trackSectionTurn for the section, jump to BCS rlin3 \ rlin3 to move on to the next track section \ If we get here, then: \ \ * turnCounter is zero \ \ * Bit 0 of thisSectionFlags is clear, so this is a \ straight track section \ \ * The front segment's number within the track \ section is less than the trackSectionTurn value \ for the section \ \ So we are not already turning from a previous call, we \ are on a straight, and we haven't yet reached the \ point at the end of the straight where we should be \ turning \ \ So now we keep on driving straight .rlin1 LDA sectionSteering,X \ Set A to the optimum steering for section X, with ORA #%01000000 \ bit 6 set, so we do not apply any steering in MoveCars \ but instead just apply acceleration or braking BNE rlin7 \ Jump to rlin7 to store A in segmentSteering for this \ section and return from the subroutine (this BNE is \ effectively a JMP as A is never zero) .rlin2 \ If we get here then this is a curved track section and \ turnCounter is zero LDA prevDriverSpeed7 \ If bit 7 of prevDriverSpeed7 is set, jump to rlin4 BMI rlin4 \ If we get here then this is a curved track section and \ turnCounter is zero, and bit 7 of prevDriverSpeed7 is \ clear .rlin3 \ If we jump here, then the front segment is past the \ trackSectionTurn point in this section, so we start \ looking at the next section for the optimum steering TYA \ Set Y = Y + 8 CLC \ ADC #8 \ So Y contains the number * 8 of the next track section TAY INX \ Increment X, so X contains the number of the next \ track section .rlin4 LDA trackSectionFlag,Y \ If bit 0 of the track section's flag byte is clear, AND #1 \ then this is a straight section, so jump to rlin1 to BEQ rlin1 \ disable steering in MoveCars and keep on driving \ straight LDA trackSectionTurn,Y \ Set turnCounter = track section's trackSectionTurn STA turnCounter BEQ rlin1 \ If the track section's trackSectionTurn is zero, jump \ to rlin1 to disable steering in MoveCars and keep on \ driving straight LDA trackDriverSpeed,Y \ Set prevDriverSpeed7 = track section's driver speed, STA prevDriverSpeed7 \ which we only use to check bit 7 AND #%01111111 \ Set prevDriverSpeed06 = bits 0-6 of the track STA prevDriverSpeed06 \ section's driver speed LDA sectionSteering,X \ Set A to the optimum steering for the track section STA previousSteering \ Set previousSteering to the optimum steering for the \ track section JMP rlin7 \ Jump to rlin7 to store A in segmentSteering for this \ section and return from the subroutine (this BNE is \ effectively a JMP as A is never zero) .rlin5 \ If we get here then turnCounter is non-zero, so a \ turn is already in progress DEC turnCounter \ Decrement the turn counter in turnCounter LDA turnCounter \ Set T = turnCounter / 8 LSR A LSR A LSR A STA T LDA turnCounter \ Set A = turnCounter - prevDriverSpeed06 SEC SBC prevDriverSpeed06 BCS rlin6 \ If the subtraction didn't underflow, i.e. \ \ turnCounter >= prevDriverSpeed06 \ \ then jump to rlin6 to set segmentSteering to \ previousSteering and return from the subroutine \ If we get here then turnCounter < prevDriverSpeed06 \ and A is negative ADC T \ Set A = A + T \ = turnCounter - prevDriverSpeed06 \ + turnCounter / 8 \ = (turnCounter * 1.125) - prevDriverSpeed06 LDA #0 \ Set A = 0 BCS rlin7 \ If the addition overflowed, then because A was \ negative, we must have the following: \ \ (turnCounter * 1.125) - prevDriverSpeed06 >= 0 \ \ So jump to rlin7 to store 0 in segmentSteering for \ this section and return from the subroutine \ \ Otherwise store previousSteering in segmentSteering \ for this section with bit 7 flipped, and return from \ the subroutine .rlin6 LDA previousSteering \ Set A = previousSteering BCS rlin7 \ If the C flag is clear, flip bit 7 of A EOR #%10000000 .rlin7 LDY frontSegmentIndex \ Set segmentSteering for the front segment in the track STA segmentSteering,Y \ segment buffer to A RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: ProcessDrivingKeys (Part 1 of 6) \ Type: Subroutine \ Category: Keyboard \ Summary: Process joystick steering \ Deep dive: Computer assisted steering (CAS) \ \ ------------------------------------------------------------------------------ \ \ This routine scans for key presses, or joystick (when configured), and updates \ the following variables accordingly: \ \ * Steering: (steeringHi steeringLo) \ \ * Brake/throttle: throttleBrakeState, throttleBrake \ \ * Gear changes: gearChangeKey, gearChange, gearNumber \ \ ****************************************************************************** .ProcessDrivingKeys LDA #0 \ Set V = 0, which we will use to indicate whether any STA V \ steering is being applied (0 indicates that none is \ being applied, which we may change later) STA T \ Set T = 0, which we will use to record whether SPACE \ is being pressed, which makes the steering wheel turn \ more quickly STA gearChangeKey \ Set gearChangeKey = 0, which we will use to indicate \ whether any gear change are being applied (0 indicates \ that no gear changes are being applied, which we may \ change later) IF _ACORNSOFT OR _4TRACKS BIT configJoystick \ If bit 7 of configJoystick is clear then the joystick BPL keys2 \ is not configured, so jump to keys2 to check for key \ presses for the steering LDX #&9D \ Scan the keyboard to see if SPACE is being pressed, as JSR ScanKeyboard \ this will affect the speed of any steering changes PHP \ Store the result of the scan on the stack ELIF _SUPERIOR OR _REVSPLUS LDX #&9D \ Scan the keyboard to see if SPACE is being pressed, as JSR ScanKeyboard \ this will affect the speed of any steering changes PHP \ Store the result of the scan on the stack BIT configJoystick \ If bit 7 of configJoystick is clear then the joystick BPL keys2 \ is not configured, so jump to keys2 to check for key \ presses for the steering ENDIF LDX #1 \ Read the joystick x-axis into A and X (A is set to the JSR GetADCChannel \ high byte of the channel, X is set to the sign of A \ where 1 = negative/left, 0 = positive/right) STA U \ Store the x-axis high byte in U JSR Multiply8x8 \ Set (A T) = A * U \ = A * A \ = A^2 \ = x-axis^2 PLP \ Retrieve the result of the keyboard scan above, when \ we scanned for SPACE BEQ keys1 \ If SPACE is being pressed, jump to keys1 so the value \ of (A T) will be four times higher LSR A \ Set (A T) = (A T) / 4 ROR T \ = x-axis^2 / 4 LSR A ROR T .keys1 IF _ACORNSOFT OR _4TRACKS PHA \ Store A on the stack so we can retrieve it later ELIF _SUPERIOR OR _REVSPLUS STA U \ Set (U T) = (A T) ENDIF LDA T \ Clear bit 0 of T AND #%11111110 STA T TXA \ Set bit 0 of T to the sign bit in X (1 = left, ORA T \ 0 = right), so this sets (A T) to the correct sign STA T \ for a steering measurement IF _ACORNSOFT OR _4TRACKS PLA \ Retrieve the value of A that we stored on the stack JMP keys11 \ Jump to the end of part 2 to update the steering value \ in (steeringHi steeringLo) to (A T) ELIF _SUPERIOR OR _REVSPLUS LDA U \ Set (A T) = (U T) \ \ so (A T) contains the joystick x-axis high byte, \ squared, divided by 4 if SPACE is not being pressed, \ and converted into a sign-magnitude number with the \ sign in bit 0 (1 = left, 0 = right) JMP AssistSteering \ Jump to AssistSteering to apply computer assisted \ steering (CAS), which in turn jumps back to keys7 or \ keys11 in part 2 NOP \ These instructions are unused, and are included to NOP \ pad out the code from when the CAS code was inserted \ into the original version ENDIF \ ****************************************************************************** \ \ Name: ProcessDrivingKeys (Part 2 of 6) \ Type: Subroutine \ Category: Keyboard \ Summary: Process keyboard steering \ Deep dive: Computer assisted steering (CAS) \ \ ------------------------------------------------------------------------------ \ \ Other entry points: \ \ keys7 Re-entry point for the AssistSteering routine when no \ joystick steering is being applied \ \ keys10 Re-entry point for the AssistSteering routine when CAS \ is being applied to the steering \ \ keys11 Re-entry point for the AssistSteering routine if CAS is \ not enabled or the car is facing backwards \ \ ****************************************************************************** .keys2 LDX #&A9 \ Scan the keyboard to see if "L" is being pressed JSR ScanKeyboard BNE keys3 \ If "L" is not being pressed, jump to keys3 LDA #2 \ Set V = 2 STA V .keys3 LDX #&A8 \ Scan the keyboard to see if ";" is being pressed JSR ScanKeyboard BNE keys4 \ If ";" is not being pressed, jump to keys4 INC V \ Set V = 1 .keys4 \ By this point, we have: \ \ * V = 1 if ";" is being pressed (steer right) \ \ * V = 2 if "L" is being pressed (steer left) \ \ * V = 0 if neither is being pressed \ \ We now calculate the amount of steering change into \ (A T), so we can apply it to (steeringHi steeringLo), \ which is a sign-magnitude number with the sign bit in \ bit 0 \ \ In the following, we swap the steering change between \ (A T) and (U T) quite a bit, and in the Superior \ Software variant of the game, we also apply computer \ assisted steering (CAS) LDA #3 \ Set U = 3 STA U \ \ So (U T) = (3 0) = 768, which is the value we use for \ steering when the SPACE key is held down IF _ACORNSOFT OR _4TRACKS LDX #&9D \ Scan the keyboard to see if SPACE is being pressed JSR ScanKeyboard ELIF _SUPERIOR OR _REVSPLUS PLP \ Retrieve the result of the keyboard scan above, when \ we scanned for SPACE ENDIF BEQ keys6 \ If SPACE is being pressed, jump to keys6 with \ (U T) = 768 \ SPACE is not being pressed, so we need to calculate \ the correct value of (U T) depending on the steering \ wheel position LDA #0 \ Set A = 0 LDX #2 \ If steeringHi > 2, jump to keys5 to skip the following CPX steeringHi \ instruction BCC keys5 LDA #1 \ Set A = 1 .keys5 STA U \ Set U = A, which will be 1 if steeringHi > 2, or \ 0 otherwise LDA #128 \ Set T = 128 STA T .keys6 \ By this point, (U T) is: \ \ * (3 0) = 768 if the SPACE key is being held down \ \ * (1 128) = 384 if steeringHi > 2 \ \ * (0 128) = 128 if steeringHi <= 2 LDA V \ If V = 0 then no steering is being applied, so jump to BEQ keys7 \ keys7 CMP #3 \ If V = 3, jump to keys13 to move on to the throttle BEQ keys13 \ and brake keys without applying any steering \ If we get here then V = 1 or 2, so steering is being \ applied, so we set the sign of (U T) to match the \ position of the wheel, before jumping down to keys8 \ or keys9 EOR steeringLo \ If bit 0 of steeringLo is clear, jump to keys9 AND #1 BEQ keys9 JSR Negate16Bit+2 \ Otherwise (steeringHi steeringLo) is negative, so \ set (A T) = -(U T) JMP keys8 \ Jump to keys8 to store the negated value in (U T) .keys7 \ If we get here then no steering is being applied LDA xTyreForceNoseLo \ Set T = xTyreForceNoseLo AND %11110000 AND #%11110000 STA T LDA xTyreForceNoseHi \ Set (A T) = (xTyreForceNoseHi xTyreForceNoseLo) \ AND %11110000 \ = xTyreForceNose AND %11110000 JSR Absolute16Bit \ Set (A T) = |A T| LSR A \ Set (A T) = (A T) >> 2 ROR T \ = |A T| / 4 LSR A \ = (|xTyreForceNose| AND %11110000) / 4 ROR T IF _ACORNSOFT OR _4TRACKS CMP #12 \ If A < 12, skip the following instruction BCC keys8 LDA #12 \ Set A = 12, so A has a maximum value of 12, and |A T| \ is set to a maximum value of 12 * 256 ELIF _SUPERIOR OR _REVSPLUS CMP steeringHi \ If A < steeringHi, clear the C flag, so the following \ call to SetSteeringLimit does nothing JSR SetSteeringLimit \ If A >= steeringHi, set: \ \ (A T) = |steeringHi steeringLo| \ \ so this is the maximum value of |A T| ENDIF .keys8 STA U \ Set (U T) = (A T) .keys9 IF _ACORNSOFT OR _4TRACKS LDA steeringLo \ Set A = steeringLo ELIF _SUPERIOR OR _REVSPLUS JMP AssistSteeringKeys \ Jump to AssistSteeringKeys, which in turn jumps back \ to keys10, so this is effectively a JSR call \ \ The routine returns with A = steeringLo ENDIF .keys10 SEC \ Set (A T) = (steeringHi steeringLo) - (U T) SBC T \ STA T \ starting with the low bytes LDA steeringHi \ And then the high bytes SBC U CMP #200 \ If the high byte in A < 200, skip the following BCC keys11 \ instructions \ Otherwise the high byte in A >= 200, so we negate \ (A T) JSR Negate16Bit \ Set (A T) = -(A T) STA U \ Set (U T) = (A T) LDA T \ Flip the sign bit in bit 0 of T EOR #1 STA T LDA U \ Set (A T) = (U T) .keys11 CMP #145 \ If the high byte in A < 145, skip the following BCC keys12 \ instruction LDA #145 \ Set A = 145, so A has a maximum value of 145 .keys12 STA steeringHi \ Set (steeringHi steeringLo) = (A T) LDA T STA steeringLo \ ****************************************************************************** \ \ Name: ProcessDrivingKeys (Part 3 of 6) \ Type: Subroutine \ Category: Keyboard \ Summary: Process joystick brake and throttle \ \ ****************************************************************************** .keys13 LDA leaveTrackTimer \ If leaveTrackTimer is non-zero then the leave track BNE keys19 \ timer is counting down, so jump to keys19 to skip the \ whole brake/throttle section BIT configJoystick \ If bit 7 of configJoystick is clear then the joystick BPL keys15 \ is not configured, so jump to keys15 to check for key \ presses for the brake/throttle LDX #2 \ Read the joystick y-axis into A and X, clearing the JSR GetADCChannel \ C flag if A < 10, setting A to the high byte, and \ setting X to 1 if the stick is up, or 0 if the stick \ is down BCC keys19 \ If A < 10 then the joystick is pretty close to the \ centre, so jump to keys19 so we don't register any \ throttle or brake activity STA T \ Set T = A / 2 LSR T ASL A \ Set A = A * 2 ADC T \ Set A = A + T \ = A * 2 + A / 2 \ = 2.5 * A BCS keys14 \ If the addition overflowed, then the joystick has \ moved a long way from the centre, so jump to keys14 to \ apply the brakes or throttle at full power CMP #250 \ If A < 250, jump to keys20 to store A in throttleBrake BCC keys20 \ and X in throttleBrakeState, so we store the amount of \ brakes or throttle to apply .keys14 CPX #0 \ If X = 0 then the joystick y-axis is positive (down), BEQ keys18 \ so jump to keys18 to apply the brakes BNE keys16 \ Otherwise the joystick y-axis is negative (up), so \ jump to keys16 to increase the throttle (this BNE is \ effectively a JMP as we already know X is non-zero) \ ****************************************************************************** \ \ Name: ProcessDrivingKeys (Part 4 of 6) \ Type: Subroutine \ Category: Keyboard \ Summary: Process keyboard brake and throttle \ \ ****************************************************************************** .keys15 LDX #&AE \ Scan the keyboard to see if "S" is being pressed (the JSR ScanKeyboard \ throttle key) BNE keys17 \ If "S" is not being pressed, jump to keys17 to check \ the next key LDX #1 \ Set X = 1 to store in throttleBrakeState below .keys16 LDA #255 \ Set A = 255 to store in throttleBrake below BNE keys20 \ Jump to keys20 (this BNE is effectively a JMP as A is \ never zero) .keys17 LDX #&BE \ Scan the keyboard to see if "A" is being pressed (the JSR ScanKeyboard \ brake key) BNE keys19 \ If "A" is not being pressed, jump to keys19 LDX #0 \ Set X = 0 to store in throttleBrakeState below .keys18 LDA #250 \ Set A = 250 to store in throttleBrake below BNE keys20 \ Jump to keys20 (this BNE is effectively a JMP as A is \ never zero) .keys19 \ If we get here then neither of the throttle keys are \ being pressed, or the joystick isn't being moved \ enough to register a change LDX #%10000000 \ Set bit 7 of X to store in throttleBrakeState below LDA revCount \ Set A = 5 + revCount / 4 LSR A \ LSR A \ to store in throttleBrake below CLC ADC #5 .keys20 STX throttleBrakeState \ Store X in throttleBrakeState STA throttleBrake \ Store A in throttleBrake \ By the time we get here, one of the following is true: \ \ * There is no brake or throttle action from keyboard \ or joystick: \ \ throttleBrakeState = bit 7 set \ throttleBrake = 5 + revCount / 4 \ \ * Joystick is enabled and 2.5 * y-axis < 250 (so \ joystick is in the zone around the centre) \ \ throttleBrakeState = 0 for joystick down, brake \ 1 for joystick up, throttle \ throttleBrake = 2.5 * y-axis \ \ * "S" (throttle) is being pressed (or joystick has \ 2.5 * y-axis >= 250): \ \ throttleBrakeState = 1 \ throttleBrake = 255 \ \ * "A" (brake) is being pressed (or joystick has \ 2.5 * y-axis >= 250) \ \ throttleBrakeState = 0 \ throttleBrake = 250 \ ****************************************************************************** \ \ Name: ProcessDrivingKeys (Part 5 of 6) \ Type: Subroutine \ Category: Keyboard \ Summary: Process joystick gear change \ \ ****************************************************************************** BIT configJoystick \ If bit 7 of configJoystick is clear then the joystick BPL keys21 \ is not configured, so jump to keys21 to skip the \ following joystick-specific gear checks LDX #0 \ Call OSBYTE with A = 128 and X = 0 to fetch the ADC LDA #128 \ channel that was last used for ADC conversion, JSR OSBYTE \ returning the channel number in Y, and the status of \ the two fire buttons in X TXA \ If bit 0 of X is zero, then no fire buttons are being AND #1 \ pressed, so jump to keys22 to check the next key BEQ keys22 LDY throttleBrakeState \ If throttleBrakeState <> 1, then the throttle is not DEY \ being applied, so jump to keys23 BNE keys23 \ If we get here then the fire button is being pressed \ and the throttle is being applied, which is the \ joystick method for changing gear, so now we jump to \ the correct part below to change up or down a gear LDA throttleBrake \ If the throttle amount is >= 200, jump to keys24 to CMP #200 \ change up a gear BCS keys24 BCC keys23 \ Otherwise jump to keys23 to change down a gear \ ****************************************************************************** \ \ Name: ProcessDrivingKeys (Part 6 of 6) \ Type: Subroutine \ Category: Keyboard \ Summary: Process keyboard gear change \ \ ****************************************************************************** .keys21 LDX #&9F \ Scan the keyboard to see if TAB is being pressed JSR ScanKeyboard BEQ keys23 \ If TAB is being pressed, jump to keys23 LDX #&EF \ Scan the keyboard to see if "Q" is being pressed JSR ScanKeyboard BEQ keys24 \ If "Q" is being pressed, jump to keys24 .keys22 \ If we get here then the gear is not being changed LDA #0 \ Set gearChange = 0 to indicate that we are not in the STA gearChange \ process of changing gear BEQ keys28 \ Jump to keys28 to return from the subroutine (this BEQ \ is effectively a JMP as A is always zero) .keys23 \ If we get here then either TAB is being pressed, or \ the joystick fire button is being pressed while the \ stick is in the "change down" part of the joystick \ range, so we need to change down a gear LDA #&FF \ Set A = -1 so we change down a gear BNE keys25 \ Jump to keys25 to change the gear .keys24 \ If we get here then either "Q" is being pressed, or \ the joystick fire button is being pressed while the \ stick is in the "change up" part of the joystick \ range, so we need to change up a gear LDA #1 \ Set A = 1 so we change up a gear .keys25 DEC gearChangeKey \ Set bit 7 of gearChangeKey (as we set gearChangeKey to \ zero above) LDX gearChange \ If gearChange is non-zero then we are already changing BNE keys28 \ gear, so jump to keys28 to return from the subroutine STA gearChange \ Set gearChange to -1 or 1 CLC \ Add A to the current gearNumber to get the number of ADC gearNumber \ the new gear, after the change CMP #&FF \ If the gear change will result in a gear of -1, jump BEQ keys26 \ to keys26 to set the gear number to 0 (the lowest gear \ number) CMP #7 \ If the new gear is not 7, jump to keys27 to change to BNE keys27 \ this gear LDA #6 \ Otherwise set A to 6, which is the highest gear number BNE keys27 \ allowed, and jump to keys27 to set this as the new \ gear number (this BNE is effectively a JMP as A is \ never zero) .keys26 LDA #0 \ If we get here then we just tried to change down too \ far, so set the number of the new gear to zero .keys27 STA gearNumber \ Store the new gear number in gearNumber JSR PrintGearNumber \ Print the new gear number on the gear stick .keys28 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: MainDrivingLoop (Part 1 of 5) \ Type: Subroutine \ Category: Main loop \ Summary: Main driving loop: Switch to the track and start the main loop \ Deep dive: Program flow of the main game loop \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ configStop Contains details of why we are exiting the driving loop: \ \ * Bit 5 set = retire from race/lap \ (SHIFT-f7 pressed) \ \ * Bit 7 and bit 6 set = pit stop \ (SHIFT-f0 pressed) \ \ * Bit 7 set and bit 6 clear = restart game \ (SHIFT and right arrow pressed) \ \ ****************************************************************************** .MainDrivingLoop JSR SetCustomScreen \ Switch to the custom screen mode, which also sets \ screenSection to -2, so the interrupt handler doesn't \ do any palette switching just yet, but leaves the \ palette mapped to black, so the screen is blank LDA #0 \ Set printMode = 0 so text printing pokes directly into STA printMode \ screen memory JSR CopyDashData \ Copy the dash data from the main game code to screen \ memory JSR DrawTrackView \ Copy the data from the dash data blocks to the screen \ to draw the track view \ \ As the screen is currently mapped to black, this \ doesn't show anything, but it does zero all the dash \ data blocks, so they are ready to be filled with the \ track view BIT configStop \ If bit 6 of configStop is set then we are returning to BVS main4 \ the track after visiting the pits, so jump to main4 \ to reset the pit stop flag and enter the driving loop .main1 \ We jump back here when restarting practice laps LDX #0 \ Zero the clock timer, as there is no time limit on the JSR ZeroTimer \ practice session .main2 \ We jump back here when restarting qualifying laps JSR ResetVariables \ Reset a number of variables for driving, and print the \ top two text lines .main3 \ We jump back here when restarting a Novice race JSR BuildPlayerCar \ Build the objects for the player's car .main4 LDA #0 \ Set configStop = 0 so we clear out any existing STA configStop \ stop-related key presses JSR ScaleWingSettings \ Scale the wing settings and calculate the wing balance \ for use in the driving model \ ****************************************************************************** \ \ Name: MainDrivingLoop (Part 2 of 5) \ Type: Subroutine \ Category: Main loop \ Summary: Main driving loop: The body of the main loop \ Deep dive: Program flow of the main game loop \ \ ****************************************************************************** .main5 \ The main driving loop starts here, and we loop back to \ here from part 5 JSR ProcessTime \ Increment the timers and the main loop counter, and \ set the speed for the next non-player drivers JSR ShowStartingLights \ If this is a race, show the starting lights on the \ right of the screen JSR ProcessDrivingKeys \ Check for and process the main driving keys JSR ApplyDrivingModel \ Apply the driving model to the player's car JSR GetTrackAndMarkers \ Calculate the coordinates for the track sides and \ corner markers JSR MovePlayerOnTrack \ Update the position of the player's car within the \ current track segment JSR MovePlayerSegment \ Move the player's car into the correct segment JSR UpdateLapTimers \ Update the lap timers and display timer-related \ messages at the top of the screen JSR MakeDrivingSounds \ Make the relevant sounds for the engine and tyres JSR ResetTrackLines \ Reset the track lines below the horizon in the track \ view JSR DrawTrack \ Draw the track into the screen buffer JSR MakeDrivingSounds \ Make the relevant sounds for the engine and tyres JSR SetBackground \ Set the background colour for any track lines in the \ track view that do not currently have a background set JSR BuildRoadSign \ Build the road sign (if one is visible) LDX #23 \ Draw the road sign we just built JSR DrawCarOrSign JSR DrawCornerMarkers \ Draw any visible corner markers JSR MoveAndDrawCars \ Move the cars around the track and draw any that are \ visible, up to a maximum of five JSR CopyTyreDashEdges \ Copy the pixels from the edges of the left tyre and \ right dashboard so they can be used when drawing the \ track view around the tyres and dashboard, and fill \ the blocks to the right of the edges with the \ appropriate content JSR UpdateMirrors \ Update the view in the wing mirrors JSR MakeDrivingSounds \ Make the relevant sounds for the engine and tyres JSR MoveHorizon \ Move the position of the horizon palette switch up or \ down, depending on the current track pitch angle JSR ProcessContact \ Process any car-on-car contact, if there has been any JSR CheckForCrash \ Check to see if we have crashed into the fence, and if \ so, display the fence, make the crash sound and set \ crashedIntoFence = &FF JSR DrawTrackView \ Copy the data from the dash data blocks to the screen \ to draw the track view, fitting it around the tyres \ and dashboard \ ****************************************************************************** \ \ Name: MainDrivingLoop (Part 3 of 5) \ Type: Subroutine \ Category: Main loop \ Summary: Main driving loop: Process rejoining the race or lap after a crash \ Deep dive: Program flow of the main game loop \ \ ****************************************************************************** LDA screenSection \ If screenSection is positive, jump to main6 to skip BPL main6 \ the following instruction INC screenSection \ If screenSection is negative, then we increment it \ \ This kickstarts the custom screen interrupt handler \ on the first time round the main driving loop, as the \ call to SetCustomScreen at the start of the routine \ sets screenSection to -2, and this increment bumps it \ up to -1, which makes the screen handler start \ applying the custom screen effect \ \ In other words, this displays the driving screen for \ the first time, after waiting for all the drawing \ routines in part 2 to finish, so we don't get to see \ the screen being drawn .main6 LDA crashedIntoFence \ If crashedIntoFence = 0 then we have not crashed into BEQ main10 \ the fence, so jump to main10 to continue with the main \ driving loop in part 5 INC crashedIntoFence \ Otherwise crashedIntoFence must be &FF, which means we \ have crashed into the fence, so increment it back to \ zero, to clear the "we have crashed" flag \ We now pause for a few seconds, before jumping back to \ the relevant starting point for the loop LDA #156 \ Set irqCounter = 156 STA irqCounter .main7 LDA irqCounter \ Fetch irqCounter, which gets incremented every time \ the IRQ routine reaches section 4 of the custom screen BMI main7 \ Loop back to main7 until irqCounter increments round \ to zero (so we wait for it to go from 156 to 0, which \ takes around three seconds at 50 ticks per second) .main8 LDA qualifyingTime \ If bit 7 of qualifyingTime is set then this is a BMI main1 \ practice lap (i.e. qualifyingTime = 255), so jump back \ to main1 LDA raceStarted \ If bit 7 of raceStarted is clear then this is either BPL main2 \ a practice or qualifying lap, but we didn't just jump \ to main1, so this must be qualifying, so jump back to \ main2 LDA raceClass \ If raceClass = 0 then this is a Novice race, so jump BEQ main3 \ back to main3 \ Otherwise this is an Amateur or a Professional race, \ and not a Novice race, practice or a qualifying lap \ ****************************************************************************** \ \ Name: MainDrivingLoop (Part 4 of 5) \ Type: Subroutine \ Category: Main loop \ Summary: Main driving loop: Leave the track \ Deep dive: Program flow of the main game loop \ \ ****************************************************************************** .main9 \ If we get here then either: \ \ * We have quit the race or lap by pressing SHIFT-f7 \ (in which case we jumped here from part 5) \ \ * This is either an Amateur or a Professional race \ and we crashed (in which case we fell through from \ part 3) \ \ * leaveTrackTimer = 1 (in which case we jumped here \ from part 5 after the leave track timer ran down) \ \ In all cases, we are done racing and need to leave \ the track JSR FlushSoundBuffers \ Flush all four sound channel buffers LDA qualifyingTime \ If bit 7 of qualifyingTime is set then this is a BMI main8 \ practice lap (i.e. qualifyingTime = 255), so jump to \ main1 via main8, so we start a new practice lap LDX #48 \ Blank out the first text line at the top of the screen JSR PrintSecondLineGap \ and print token 48 on the second line, to give: \ \ " " \ " PLEASE WAIT " JSR FinishRace \ Continue running the race until all the non-player \ drivers have finished and we have a result LDA configStop \ If bit 7 of configStop is set then we must be pressing BMI main13 \ either SHIFT-f0 for a pit stop or SHIFT and right \ arrow to restart the game, so jump to main13 to leave \ the track LDA #%00100000 \ Set bit 5 of configStop to indicate that we have STA configStop \ retired from the race (so we leave the track \ permanently rather than just visiting the pits) BNE main13 \ Jump to main13 to leave the track (this BNE is \ effectively a JMP as A is never zero) \ ****************************************************************************** \ \ Name: MainDrivingLoop (Part 5 of 5) \ Type: Subroutine \ Category: Main loop \ Summary: Main driving loop: Process driving keys, potentially leaving the \ track, and loop back to part 2 \ Deep dive: Program flow of the main game loop \ \ ****************************************************************************** .main10 IF _ACORNSOFT OR _4TRACKS LDY #9 \ Check for all the shifted keys (i.e. those that need JSR ProcessShiftedKeys \ SHIFT holding down to trigger) and process them \ accordingly ELIF _SUPERIOR OR _REVSPLUS LDY #11 \ Check for all the shifted keys (i.e. those that need JSR ProcessShiftedKeys \ SHIFT holding down to trigger) and process them \ accordingly ENDIF LDA configStop \ If configStop = 0, then we aren't pressing one of the BEQ main11 \ keys that stops the race, so jump to main11 to keep \ iterating round the main driving loop BPL main9 \ If bit 7 of configStop is clear then we must be \ pressing SHIFT-f7 to retire from the race, so jump to \ main9 to leave the track AND #%01000000 \ If bit 6 of configStop is clear (and we know bit 7 is BEQ main13 \ set), then we must be pressing SHIFT and right arrow, \ so jump to main13 to leave the track and restart the \ game \ If we get here then we know both bit 6 and bit 7 must \ be set, so we must be pressing SHIFT-f0 to return to \ the pits LDA playerMoving \ If playerMoving = 0 then the player's car is BEQ main13 \ stationary, so jump to main13 to leave the track and \ return to the pits LDA #0 \ We can't enter the pits if the car is moving, so set STA configStop \ configStop = 0 so we clear out the SHIFT-f4 key press .main11 LDX leaveTrackTimer \ Set X to the current value of the leave track timer BEQ main12 \ If X = 0 then the leave track timer is not running, so \ jump to main12 to continue with the main driving loop DEX \ The leave track timer is running, so decrement the \ timer value in X BEQ main9 \ If X = 0 then the leave track timer was 1 before the \ decrement and has now run down, so jump to main9 to \ leave the track STX leaveTrackTimer \ Store the decremented leave track timer so that it \ continues counting down towards 1 .main12 JSR MakeDrivingSounds \ Make the relevant sounds for the engine and tyres JSR UpdateDashboard \ Update the rev counter on the dashboard JMP main5 \ Loop back to main5 to repeat the main driving loop .main13 \ If we get here then we leave the track and switch back \ to mode 7, either to visit the pits or because the \ driving is done LDA #%10000000 \ Copy the dash data from screen memory back to the main JSR CopyDashData \ game code JSR KillCustomScreen \ Disable the custom screen mode and switch to mode 7 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: AddTimeToTimer \ Type: Subroutine \ Category: Drivers \ Summary: Add time to the specified timer \ Deep dive: Scheduling tasks in the main loop \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The timer to increment: \ \ * 0 = the clock timer \ (clockMinutes clockSeconds clockTenths) \ \ * 1 = the lap timer \ (lapMinutes lapSeconds lapTenths) \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ C flag Denotes whether the number of seconds has changed: \ \ * Set if the time just ticked on to the next second \ \ * Clear if the time is unchanged \ \ ****************************************************************************** .AddTimeToTimer SED \ Set the D flag to switch arithmetic to Binary Coded \ Decimal (BCD) LDA #&09 \ Set A = &09, so we add 9/100 of a second below LDY timerAdjust \ If timerAdjust <> trackTimerAdjust (which is 24 for CPY trackTimerAdjust \ the Silverstone track), jump to time1 to skip the BNE time1 \ following \ If we get here then timerAdjust = trackTimerAdjust, so \ we need to apply the clock adjustment, as this gets \ applied every trackTimerAdjust iterations around the \ main driving loop \ \ The clock adjustment speeds the clock up by advancing \ the clock twice as fast as usual, for this tick only LDA #&18 \ Set A = &18, so we add 18/100 of a second below .time1 CLC \ Add A to the tenths for the timer ADC clockTenths,X \ STA clockTenths,X \ starting with the tenths of a second PHP \ Store the C flag on the stack, so we can return it \ from the subroutine below (the C flag will be set \ if the lap time just ticked on to the next second) LDA clockSeconds,X \ Then we add the seconds into A ADC #0 CMP #&60 \ If A < &60, then the number of seconds is still valid, BCC time2 \ so jump to time2 to skip the following instruction LDA #0 \ Otherwise set A = 0, so 60 seconds on the timer \ increments back round to 0 seconds .time2 STA clockSeconds,X \ Update the seconds value for the timer LDA clockMinutes,X \ Finally, we add the minutes ADC #0 STA clockMinutes,X BPL time3 \ If the updates minutes value for the timer is \ positive, jump to time3 to skip the following JSR ZeroTimer \ Otherwise the timer just reached the maximum possible \ value, so wrap it back round to zero LDY currentPlayer \ Set the total race time for the current player to &80, LDA #&80 \ or 80 minutes (as totalRaceMinutes is BCD), otherwise STA totalRaceMinutes,Y \ the driver might finish with a very low time just \ because the race timer looped back to zero .time3 PLP \ Retrieve the C flag from the stack, which will be set \ if the addition of tenths overflowed (in other words, \ if the lap time just ticked on to the next second) CLD \ Clear the D flag to switch arithmetic to normal RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: PrintSecondLineGap \ Type: Subroutine \ Category: Text \ Summary: Prints a text token on the second text line at the top of the \ driving screen, with an empty gap on the line above \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The token number (0 to 54) to print on the second text \ line at the top of the screen \ \ ****************************************************************************** .PrintSecondLineGap JSR PrintSecondLine \ Print token X on the second text line at the top of \ the screen LDX #45 \ Print token 45 (38 spaces) on the first text line of JSR PrintFirstLine \ at the top of the screen, which blanks the top line RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: ResetVariables \ Type: Subroutine \ Category: Main Loop \ Summary: Reset a number of variables for driving, and print the top two \ text lines \ \ ****************************************************************************** .ResetVariables LDA #0 \ Set A = 0, so we can use it to reset variables to zero \ in the following loops LDX #&68 \ We start by zeroing all zero-page variables from \ playerMoving to processContact, so set up a loop \ counter in X .rese1 STA playerMoving,X \ Zero the X-th byte from playerMoving DEX \ Decrement the loop counter BPL rese1 \ Loop back until we have zeroed all variables from \ playerMoving to processContact LDX #&7F \ We now zero all variables from xPlayerCoordHi to \ zTyreForceBoth, so set up a loop counter in X .rese2 STA xPlayerCoordHi,X \ Zero the X-th byte from xPlayerCoordHi DEX \ Decrement the loop counter BPL rese2 \ Loop back until we have zeroed all variables from \ xPlayerCoordHi to zTyreForceBoth JSR DefineEnvelope \ Define the first (and only) sound envelope LDX #23 \ We now zero the 24-byte blocks at objTrackSection and \ objSectionSegmt, and initialise all 24 bytes in \ (objectSegmentHi objectSegmentLo), so set up a loop \ counter in X STX previousSignNumber \ Set previousSignNumber = 23 to set the number of the \ previous sign to the highest possible value (as the \ track sections are numbered 0 to 23), so we skip the \ part of BuildRoadSign that reuses the sign from the \ previous track section .rese3 LDA trackStartLine+1 \ Set the X-th byte of (objectSegmentHi objectSegmentLo) STA objectSegmentHi,X \ to the value of trackStartLine, which is 843 for the LDA trackStartLine \ Silverstone track and contains the segment number of STA objectSegmentLo,X \ the starting line, expressed as the number of segments \ between the starting line and the start of section 0, \ counting forwards round the track LDA #0 \ Zero the X-th byte of objTrackSection to put the STA objTrackSection,X \ object in the first track section, ready for the call \ to PlaceCarsOnTrack below STA objSectionSegmt,X \ Zero the X-th byte of objSectionSegmt to put the \ object at the first segment in the current track \ section, ready for the call to PlaceCarsOnTrack below DEX \ Decrement the loop counter BPL rese3 \ Loop back until we have zeroed or copied all 24 \ variable bytes JSR SetPlayerPositions \ Set the current player's position in currentPosition, \ plus the number of the position ahead in positionAhead \ and number of the position behind in positionBehind LDA #1 \ Set A = 1, to pass to PlaceCarsOnTrack as the spacing \ for the cars when this is a race BIT raceStarted \ If bit 7 of raceStarted is set then this is a race BMI rese4 \ rather than practice or qualifying, so jump to rese4 \ to skip the following \ This is a practice or qualifying lap, so we set the \ player's position as specified in the track data LDX trackStartPosition \ Set X to trackStartPosition, the starting position for \ practice or qualifying laps, which is 4 for the \ Silverstone track LDY currentPosition \ Set Y to the current player's position JSR SwapDriverPosition \ Swap the positions of drivers in positions X and Y in \ the driversInOrder table, so for Silverstone this sets \ the current player's position to 4 (i.e. fifth place) JSR SetPlayerPositions \ Set the current player's position in currentPosition, \ plus the number of the position ahead in positionAhead \ and number of the position behind in positionBehind LDA trackCarSpacing \ Set A to trackCarSpacing, to pass to PlaceCarsOnTrack \ as the spacing for the cars when this is a qualifying \ lap (this value is 40 for the Silverstone track) .rese4 \ By this point, A = 1 if this is a race, or the value \ of trackCarSpacing if this is practice or qualifying \ (40 for Silverstone) JSR PlaceCarsOnTrack \ Reset the cars on the track, placing them on the \ starting grid if this is a race, or spread out along \ the track if this is a qualifying lap LDX #19 \ We now zero the 20-byte blocks at driverLapNumber, \ carSteering, carProgress, (carSpeedHi carSpeedLo) and \ carStatus, and initialise the 20-byte blocks at \ objectStatus, totalRaceMinutes and carSectionSpeed, \ so set up a loop counter in X .rese5 LDA #%10000000 \ Set the X-th byte of objectStatus to %10000000, so the STA objectStatus,X \ object is hidden STA totalRaceMinutes,X \ Set the X-th byte of totalRaceMinutes to &80, which is \ 80 minutes as totalRaceMinutes is in BCD LDA #0 \ Zero the X-th byte of driverLapNumber, so each driver STA driverLapNumber,X \ starts at lap 0 STA carSteering,X \ Zero the X-th byte of carSteering to set each car's \ steering to dead ahead STA carProgress,X \ Zero the X-th byte of carProgress to reset each car's \ progress within the starting segment STA carSpeedHi,X \ Zero the X-th byte of carSpeedHi to reset each car's \ forward speed STA carStatus,X \ Zero the X-th byte of carStatus so each car is ready \ to have its status byte updated and is not braking or \ accelerating STA carSpeedLo,X \ Zero the X-th byte of carSpeedLo to reset each car's \ forward speed LDA #255 \ Set the X-th byte of carSectionSpeed to 255, so there STA carSectionSpeed,X \ is no minimum speed set for the car in the next track \ section DEX \ Decrement the loop counter BPL rese5 \ Loop back until we have zeroed or initialised all 20 \ bytes in each block LDA #1 \ Set gearNumber = 1, to set the gears to neutral STA gearNumber STA pastHalfway \ Set pastHalfway = 1, so the player is in the second \ half of the track (which is true for both practice \ and qualifying, as well as the starting grid) LDX #7 \ Set oddsOfEngineStart = 7, so the odds of the engine STX oddsOfEngineStart \ starting when revving are 1 in 7 DEX \ Set sectionListValid = 6, to denote that there are no STX sectionListValid \ valid entries in the track section list STX sectionListPointer \ Set sectionListPointer = 6, to set the current entry \ to the entry after the end of the list (as the list \ is empty) DEX \ We now zero the six bytes at mirrorContents to clear \ all six segments of the wing mirrors, so set X = 5 to \ use as a loop counter .rese6 STA mirrorContents,X \ Clear the contents of the X-th wing mirror segment DEX \ Decrement the loop counter BPL rese6 \ Loop back until we have zeroed all six mirror segments JSR PrintGearNumber \ Print the new gear number on the gear stick (neutral) LDA raceStarted \ If bit 7 of raceStarted is set then this is a race BMI rese7 \ rather than practice or qualifying, so jump to rese7 \ with bit 7 of A set and bit 6 of A clear, to print the \ race header at the top of the screen LDX #40 \ Blank out the first text line at the top of the screen JSR PrintSecondLineGap \ and print token 40 on the second line, to give: \ \ " " \ "Lap Time : Best Time " LDX #1 \ Zero the lap timer JSR ZeroTimer JSR PrintBestLapTime \ Print the best lap time and the current lap time at \ the top of the screen LDA #&DF \ Set firstLapStarted = -33, so that when we add 33 in STA firstLapStarted \ the UpdateLapTimers routine, we get a value of 0 when \ we start the first lap RTS \ Return from the subroutine .rese7 STA updateDrivingInfo \ Set bit 7 and clear bit 6 of updateDrivingInfo so the \ lap number gets printed at the top of the screen STA updateDriverInfo \ Set bit 7 of updateDriverInfo so the driver names get \ printed at the top of the screen LDX #43 \ Print token 43 on the first text line at the top of JSR PrintFirstLine \ the screen and token 44 on the second line, to give: LDX #44 \ JSR PrintSecondLine \ "Position In front: " \ "Laps to go Behind: " \ \ Token 44 includes five extra spaces at the end, though \ I'm not sure why LDA currentPosition \ Set A to the current player's position JSR ConvertNumberToBCD \ Convert the number in A into binary coded decimal \ (BCD), adding 1 in the process STA positionChangeBCD \ Set positionChangeBCD to the current player's position \ in BCD RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: SetBackground \ Type: Subroutine \ Category: Screen buffer \ Summary: Set the background colour for any track lines that have not yet \ had a background colour set \ Deep dive: Drawing the track view \ \ ****************************************************************************** .SetBackground \ We start by setting the colour of the track line that \ contains the horizon LDX horizonListIndex \ Set X = horizonListIndex \ \ So X is the index in the track verge buffer for the \ horizon line's right verge LDY horizonLine \ Set Y to the track line number of the horizon LDA xVergeLeftHi,X \ Set A = X-th entry in xVergeLeftHi + 20 CLC ADC #20 BPL bgnd1 \ If A is positive, then the left edge of the verge is \ on-screen, so jump to bgnd1 to set the lines below \ the horizon to the colour of grass LDA xVergeRightHi,X \ Set A = X-th entry in xVergeRightHi + 20 CLC ADC #20 BMI bgnd1 \ If A is negative, then the right edge of the verge is \ on-screen, jump to bgnd1 to set the lines below \ the horizon to the colour of grass \ If we get to this point, then: \ \ * xVergeRightHi + 20 is positive \ \ * xVergeLeftHi + 20 is negative \ \ Adding 20 degrees to the yaw angles will move them to \ the right by half the screen width, so this is the \ same as moving the angles from the left edge of the \ screen to the middle \ \ We then check whether moving the angles to the centre \ pushes the rightmost verge edge in A past the centre \ (i.e. positive), while still leaving the leftmost edge \ in the left half (i.e. negative) \ \ If so, then this means the verge at the horizon is \ straddling the left edge of the screen, so we need to \ set the background colour for the horizon track line \ to black LDA #%00100000 \ Set A = %00100000 (colour 0, black) for the horizon \ track line, where: \ \ * %00 in bits 0-1 is logical colour 0 \ \ * %001xx0xx denotes that this value was stored in \ the backgroundColour table by the SetBackground \ routine BNE bgnd2 \ Jump to bgnd2 (this BNE is effectively a JMP as A is \ never zero) .bgnd1 LDA #%00100011 \ Set A = %00100011 (colour 3, green) for the horizon \ track line, where: \ \ * %11 in bits 0-1 is logical colour 3 \ \ * %001xx0xx denotes that this value was stored in \ the backgroundColour table by the SetBackground \ routine .bgnd2 STA backgroundColour,Y \ Set the colour of the horizon line to A \ We now work our way through the whole backgroundColour \ table, filling in any entries that are zero (and which \ have therefore not been set yet) with blue LDA #%00100001 \ Set A = %00100001 (colour 1, blue) to use as the line \ colour for lines above the horizon, i.e. the sky, \ where: \ \ * %01 in bits 0-1 is logical colour 1 \ \ * %001xx0xx denotes that this value was stored in \ the backgroundColour table by the SetBackground \ routine LDY #79 \ We now loop through all the track lines, starting from \ line 79 at the top of the track view down to 0 at the \ bottom, so set a counter in Y \ \ If any values are zero, then we set them to blue .bgnd3 LDX backgroundColour,Y \ Set X to the background colour for track line Y BEQ bgnd4 \ If X is zero, then this track line doesn't currently \ have a background colour set, so jump to bgnd4 to set \ the track line to blue TXA \ If we get here then X is non-zero, so there is already \ a backgroundColour value for this track line, so copy \ this value to A so the following STA instruction \ doesn't change anything .bgnd4 STA backgroundColour,Y \ Set the background colour for track line Y to A DEY \ Decrement the loop counter to move down to the next \ track line BPL bgnd3 \ Loop back until we have set the line colour for all \ 80 track lines RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: CopyDashData \ Type: Subroutine \ Category: Screen buffer \ Summary: Copy the dash data from the main game code to screen memory, and \ vice versa \ Deep dive: The jigsaw puzzle binary \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ A The direction of the copy: \ \ * Bit 7 clear = copy from game code to screen memory \ \ * Bit 7 set = copy from screen memory to game code \ \ ****************************************************************************** .CopyDashData STA T \ Store A in T, so bit 7 of T determines the direction \ of the copy LDX #3 \ First, we copy the four bytes at dashDataAddress to \ P, Q, R and S, so set a loop counter in X for the four \ bytes .dash1 LDA dashDataAddress,X \ Copy the X-th byte of dashDataAddress to the X-th STA P,X \ byte of P DEX \ Decrement the loop counter BPL dash1 \ Loop back until we have copied all four bytes \ So we now have the following: \ \ (Q P) = the location of the first block in the main \ game code \ \ (S R) = the location of the first block in screen \ memory (i.e. at the end of screen memory, as \ the first block in the game code is the last \ block in screen memory) \ \ We now copy 41 blocks of memory from one address to \ the other, with the direction determined by the value \ of bit 7 in T (which we set above) \ \ We work through each block by starting at offset 79 \ from the start of (Q P) and (S R), and decrementing \ the offset until it matches the dashDataOffset value \ for this block \ \ We store the block number (which goes from 0 to 40) \ in X, and the offset (which goes from 79 down to \ dashDataOffset,X + 1) in Y LDX #0 \ Set a block counter in X to count through 41 blocks .dash2 LDY #79 \ Each block we want to copy ends at the start address \ plus 79, so set an index counter in Y, which we can \ use to work our way backwards through each byte in \ the block LDA #0 \ Set V = 0 to act as a byte counter to go from 0 to the STA V \ number of bytes copied .dash3 BIT T \ If bit 7 of T is set, skip the following two BMI dash4 \ instructions so we copy from (S R) to (Q P) LDA (P),Y \ Copy the Y-th byte of (Q P) to the Y-th byte of (S R) STA (R),Y .dash4 LDA (R),Y \ Copy the Y-th byte of (S R) to the Y-th byte of (Q P) STA (P),Y INC V \ Increment the byte counter DEY \ Decrement the index counter TYA \ If Y <> the dashDataOffset value for block X, loop CMP dashDataOffset,X \ to keep copying the contents of this block BNE dash3 \ We have copied a block of memory, so we now need to \ update (Q P) and (S R) to point to the next block to \ copy \ \ When in screen memory, the blocks are stored one after \ the other, in reverse order, so the address of the \ next block to copy is the start address of the block \ we just copied in (S R), minus the size of the block \ we just copied, which is in V, so the next block will \ be at (S R) - V LDA R \ Set (S R) = (S R) - V SEC \ SBC V \ starting with the low bytes STA R BCS dash5 \ And decrementing the high byte of (S R) if the low DEC S \ byte wraps around .dash5 \ When in the main game code, the blocks are stored \ every &80 bytes, so the address of the next block is \ (Q P) + &80 \ \ Note that each block takes up a different amount of \ memory, as follows: \ \ Block starts at: (Q P) + dashDataOffset,X + 1 \ Block ends at: (Q P) + 79 \ \ It's the value of (Q P) that is spaced out by &80 for \ each block, rather than the actual data in the block \ (for each block, (Q P) to (Q P) + dashDataOffset,X \ is used for other purposes) LDA P \ Set (Q P) = (Q P) + &80 CLC \ ADC #&80 \ starting with the low bytes STA P BCC dash6 \ And incrementing the high byte of (Q P) if the low INC Q \ byte overflows .dash6 INX \ Increment the block counter to point to the next block \ to copy CPX #41 \ Loop back to copy the next block until we have copied BNE dash2 \ all 41 blocks RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: dashDataAddress \ Type: Variable \ Category: Screen buffer \ Summary: Addresses for copying the first block of dash data between the \ main game code and screen memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashDataAddress EQUW dashData \ The location of the first block in the game code EQUW &8000 - 80 \ The location of the first block in screen memory \ ****************************************************************************** \ \ Name: CheckVergeOnScreen \ Type: Subroutine \ Category: Drawing the track \ Summary: Check whether a verge coordinate is on-screen \ Deep dive: Drawing the track verges \ \ ------------------------------------------------------------------------------ \ \ This routine tests whether the magnitude of a signed yaw angle is < 20. In \ other words, given a signed yaw angle x, this tests whether |x| < 20. \ \ As the field of view in Revs is 20 degrees, this tests whether or not a yaw \ angle is visible on-screen. \ \ It does this by adding 20 and then testing against 40, which gives the result \ we want as the following are all equivalent: \ \ -20 < x < 20 \ \ -20 + 20 < x < 20 + 20 \ \ 0 < x < 40 \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The index within the track verge buffer of the verge to \ check: \ \ * horizonListIndex + 40 for the left verge \ \ * horizonListIndex for the right verge \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ V The result, where x is the verge yaw angle: \ \ * Bit 7 is clear if |x| < 20 (visible on-screen) \ \ * Bit 7 is set if |x| >= 20 (not visible) \ \ ****************************************************************************** .CheckVergeOnScreen LDA xVergeRightHi,X \ Set A to the x-coordinate of the X-th entry in the \ track segment list CLC \ Set A = A + 20 ADC #20 CMP #40 \ Set bit 7 if A >= 40, clear it if A < 40 ROR V RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: MapSegmentsToLines \ Type: Subroutine \ Category: Drawing the track \ Summary: Map verges in the track segment list to track lines in the track \ view \ Deep dive: Drawing the track verges \ \ ------------------------------------------------------------------------------ \ \ This routine populates the leftSegment or rightSegment table (depending on the \ arguments). \ \ It does this by working its way through the verge buffer, from distant entries \ at the start of the buffer, to closer entries at the end of the buffer. As it \ goes, it looks at the pitch angles of the entries, which equate to track lines \ in the track view, with high track lines matching high pitch values (which are \ higher up the screen). It then fills the corresponding entries in the \ leftSegment or rightSegment table, which have one entry per track line, with \ the index numbers of the relevant entries from the verge buffer. \ \ In other words, if we fill five entries in the rightSegment table with an \ index n, then that means that the segment that's mapped to entry n in the \ verge buffer will take up five track lines on-screen, so it will be five \ pixels tall. \ \ It's worth reiterating that the track verge buffer stores distant segments \ first, coming towards us as we progress through the list, so leftSegment \ and rightSegment are the reverse of this, with closest segments at the start, \ furthest segments at the end. This matches the track lines, where small \ numbers are at the bottom of the screen (i.e. close), high numbers are up \ the screen (i.e. further away). \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ A The low byte of the table address to populate: \ \ * LO(rightSegment) = populate rightSegment \ \ * LO(leftSegment) = populate leftSegment \ \ Y Index of the last entry in the track verge buffer: \ \ * segmentListRight for the right verge \ \ * segmentListPointer for the left verge \ \ X The index within the track verge buffer of the horizon: \ \ * horizonListIndex for the right verge \ \ * horizonListIndex + 40 for the left verge \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ vergeDepthOfField Updated to skip any segments that are hidden, so we do \ not waste time drawing their verges \ \ N Contains the pitch angle of the last segment to be \ mapped, which is the closest one to the player \ \ ****************************************************************************** .MapSegmentsToLines STA maps5+1 \ Modify the instruction at maps5 to use the low byte of \ the address in A, so the STA instruction writes to the \ table specified by A STY vergeBufferEnd \ Set vergeBufferEnd to the index in Y, which points to \ the last entry in the track verge buffer for this side \ of the track DEY \ Set U = Y - 1 STY U \ \ In the following loop, X iterates up to U - 1, so this \ ensures that it increments to the end of the track \ verge buffer, but not past the end JSR CheckVergeOnScreen \ Set bit 7 of V if the segment in X is off-screen, or \ clear bit 7 if it is on-screen LDY horizonLine \ Set Y to the track line number of the horizon JMP maps7 \ Jump to maps7 to join the loop below \ \ The loop below uses two loop counters: \ \ * Y is an index into leftSegment or rightSegment, \ starting at horizonLine and decrementing down \ through the track lines, i.e. the ones that show \ land rather than sky \ \ Y decrements from the horizon track line to 1 \ \ * X is an index into xVergeRightHi and yVergeRight, \ starting at horizonListIndex (the entry in the \ verge buffer that's on the horizon) and \ incrementing though the verge buffer, going from \ distant segments back towards the player \ \ X increments from the horizon entry in the verge \ buffer, up to the end of the verge buffer (as per \ the value that we gave U above) \ \ In other words, we work our way through the verge \ buffer from the horizon towards the player, inserting \ values into the leftSegment or rightSegment table \ for each corresponding track line, starting at the \ horizon track line and working down the screen \ \ We also enter the loop with V set to the visibility of \ the horizon's entry in the verge buffer .maps1 \ We loop back here when X is incremented to point to \ the next entry in the verge buffer LDA V \ If bit 7 of V is clear, then we have already reached a BPL maps2 \ visible entry in the verge buffer, so jump to maps2 \ Bit 7 of V is set, so we haven't yet reached a visible \ entry in the verge buffer, so we test the new entry in \ X to see if it is visible \ \ In this way, V is a flag that records when we reach \ the visible entries in the buffer, at which point we \ flip bit 7 of V from set to clear, and stop checking JSR CheckVergeOnScreen \ Set bit 7 of V if the segment in X is off-screen, or \ clear bit 7 if it is on-screen .maps2 LDA yVergeRight,X \ Set A to the pitch angle of the current entry in the \ verge buffer CMP #80 \ If A >= 80, then this pitch angle maps to a track line BCS maps9 \ that's off the top of the screen, so jump to maps9 to \ pad out the rest of the leftSegment or rightSegment \ table and finish off BIT V \ If bit 7 of V is clear, then we have already reached a BPL maps3 \ visible entry in the verge buffer, so jump to maps3 \ If we get here then bit 7 of V is set, so the current \ entry in the verge buffer is not on-screen in the \ x-axis, though it is on-screen in the y-axis as we \ know A < 80 CMP yVergeRight+1,X \ If the pitch angle of this entry in the verge buffer BEQ maps14 \ is the same as the angle of the next entry (i.e. \ the next entry closer to the player), then jump to \ maps14 to set bit 7 of this entry's vergeDataRight \ and move on to the next entry in the verge buffer .maps3 \ If we get here then either this entry in the verge \ buffer is on-screen, or it's off-screen but is at a \ different pitch angle to the next closest entry CMP N \ If A >= N, then the entry is at a higher pitch angle BCS maps14 \ than the current track line in N, so jump to maps14 \ to set bit 7 of the X-th vergeDataRight and on to \ maps8 to move on to the next X \ Otherwise we fall through to fill index Y down to \ index A + 1 with the value in X, which sets the \ leftSegment or rightSegment entries for track lines \ Y down to A + 1 with the index in X .maps4 \ If we get here then we fill index Y down to index \ A + 1 with the value in X STA RR \ Set RR = A TXA \ Set A = X JMP maps6 \ Jump to maps6 to fill index Y down to index RR + 1 \ with the value in A .maps5 STA leftSegment,Y \ Store A in the Y-th entry in leftSegment or \ rightSegment DEY \ Decrement the loop counter in Y to point to the next \ track line down the screen .maps6 \ This loop fills leftSegment or rightSegment with the \ value in A, from index Y down to index RR + 1 CPY RR \ Loop back to maps5 until we have filled from index Y BNE maps5 \ to RR + 1 with the value in A .maps7 \ This is where we first join the loop STY N \ Set N = Y, so N contains the number of the track line \ we are currently processing .maps8 INX \ Increment the loop counter in X to point to the next \ entry in the verge buffer BMI maps11 \ If bit 7 of X is set, then we must have called the \ loop from maps10 below, so we have now finished \ filling the leftSegment or rightSegment table all the \ way back to index 1, so jump to maps11 to exit the \ loop as we are done filling CPX U \ If X < U, then we still have entries in the verge BCC maps1 \ buffer to process, so loop back to maps1 .maps9 \ If we get here then we are nearly done, and just need \ to pad out the rest of the leftSegment or rightSegment \ table with the current entry's index with bit 7 set, \ working back to position 1 in the table \ First we cap the pitch angle of the current entry in \ the verge buffer to a maximum of N LDA yVergeRight,X \ Set A to the pitch angle of the current entry in the \ verge buffer (i.e. the last entry we will fill, which \ is the closest track line to the player) BMI maps10 \ If A is negative, jump to maps10 CMP N \ If A < N, jump to maps10 BCC maps10 \ If we get here then A is positive and A >= N LDA N \ Set the pitch angle of the current entry in the verge STA yVergeRight,X \ buffer to N .maps10 \ We now get ready to loop back to the fill loop above, \ to pad out the remainder of the leftSegment or \ rightSegment table TXA \ Set bit 7 of X, so when we jump back to the fill loop ORA #%10000000 \ via maps4, we fill the rest of the leftSegment or TAX \ rightSegment table with this value, and then exit the \ loop by jumping to maps11 LDA #0 \ Set A = 0, so we fill the leftSegment or rightSegment \ table back to position 1 with the value in X BEQ maps4 \ Jump to maps4 to fill index Y down to index A + 1 with \ the value in X (this BEQ is effectively a JMP as A is \ always zero) .maps11 \ Now we loop X from vergeDepthOfField up until we find \ the first vergeDataRight entry with bit 7 clear, and \ set vergeDepthOfField to the updated X \ \ This moves through the verge buffer, towards the \ player, starting at the horizon line, and skipping any \ entries that have bit 7 set in vergeDataRight, so we \ set the depth of field to skip these segments \ \ We set bit 7 of vergeDataRight for any entries whose \ pitch angles were higher than the current track line \ as we worked down the screen and towards the player in \ the verge buffer \ \ So this stops us from displaying verges on any hills \ between the horizon and the player LDX vergeDepthOfField \ Set X = to the current verge depth of field, which is \ the index within the verge buffer beyond which we do \ not draw verge marks .maps12 LDA vergeDataRight,X \ If bit 7 the X-th vergeDataRight is clear, jump to BPL maps13 \ maps13 to store X in vergeDepthOfField INX \ Increment the loop counter in X CPX U \ If X < U, loop back to maps12 BCC maps12 .maps13 STX vergeDepthOfField \ Set vergeDepthOfField = X RTS \ Return from the subroutine .maps14 LDA #%10000000 \ Set bit 7 of vergeDataRight for the current entry in ORA vergeDataRight,X \ the verge buffer STA vergeDataRight,X BMI maps8 \ Jump to maps8 (this BMI is effectively a JMP as bit 7 \ of A is always set) \ ****************************************************************************** \ \ Name: DrawVergeEdge \ Type: Subroutine \ Category: Drawing the track \ Summary: Draw one of the four track verge edges into the screen buffer \ Deep dive: Drawing the track verges \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ A The index in the verge buffer where we start drawing the \ track verges (so this is essentially the depth of field \ for this verge) \ \ Y Determines the type of verge that we are drawing: \ \ * 0 = leftVergeStart, the left edge of the left verge \ \ * 1 = leftTrackStart, the right edge of the left verge \ \ * 2 = rightVergeStart, the left edge of the right \ verge \ \ * 3 = rightGrassStart, the right edge of the right \ verge \ \ vergeBufferEnd The index of the last entry in the track verge buffer \ for the side of the track we are currently drawing \ \ pixelMaskVerge Offset within the vergePixelMask table for segments that \ are close enough to show a verge \ \ * 0 for leftVergeStart \ \ * 8 for leftTrackStart \ \ * 16 for rightVergeStart \ \ * 28 for rightGrassStart \ \ pixelMaskNoVerge Offset within the vergePixelMask table for segments that \ are too far away to show a verge \ \ * n/a for leftVergeStart \ \ * 0 for leftTrackStart \ \ * 28 for rightVergeStart \ \ * 28 for rightGrassStart \ \ MM Set to 0, to act as the low byte of (NN MM) \ \ R Set to 0, to act as the low byte of (S R) \ \ P Set to &80, to use as the low byte of (Q P) \ \ ****************************************************************************** .DrawVergeEdge STY vergeType \ Set vergeType to the type of verge that we are \ going to draw STA prevPitchIndex \ Set prevPitchIndex to the verge buffer index in A, to \ use as the starting index into the buffer for the \ drawing process CMP vergeBufferEnd \ If A >= vergeBufferEnd, then it points to an index BCS vedg6 \ after the end of the verge buffer data for this side \ of the track, so jump to vedg6 to return from the \ subroutine CLC \ Set: ADC vergeEdgeInOut,Y \ STA prevYawIndex \ prevYawIndex = A + vergeEdgeInOut \ \ for the type of verge we are drawing \ \ This sets prevYawIndex to the following: \ \ * A for the inner edges of the verge mark in \ leftTrackStart and rightVergeStart \ \ * A + 16 for the outer edges of the verge mark in \ leftVergeStart and rightGrassStart \ \ This points prevYawIndex to the correct part of the \ track segment list for this entry in the buffer, as \ the angles for the outer edge of the verge mark are \ stored 16 bytes after the inner edge angles (the inner \ edge angles are in bytes 6 to 21 of the track segment \ list, while the outer edge angles are in bytes 22 to \ 37) LDA vergeTableHi,Y \ Modify the following instructions at verl2 and verb2, STA verl2+2 \ depending on the type of verge in Y: STA verb2+2 \ LDA vergeTableLo,Y \ * 0 = STA &7000,Y -> STA leftVergeStart,Y STA verl2+1 \ * 1 = STA &7000,Y -> STA leftTrackStart,Y STA verb2+1 \ * 2 = STA &7000,Y -> STA rightVergeStart,Y \ * 3 = STA &7000,Y -> STA rightGrassStart,Y \ \ So this modifies the DrawVergeByteLeft and \ DrawVergeByteRight routines to draw the verge \ specified in Y LDX prevYawIndex \ Set X = prevYawIndex, to use as the index to the \ verge's yaw angles LDY prevPitchIndex \ Set Y = prevPitchIndex, to use as the index to the \ verge's pitch angles SEC \ Set the C flag to pass to DrawSegmentEdge so it does \ not draw the edge (as this is the first call for this \ edge so we just set up the variables, ready for the \ next call) JSR DrawSegmentEdge \ Draw the verge edge for this entry (i.e. this segment) \ in the verge buffer .vedg1 INX \ Increment X to the index of the next yaw angle in the \ track segment list INY \ Increment Y to the index of the next pitch angle in \ the track segment list CPY vergeBufferEnd \ If Y >= vergeBufferEnd then we have processed all the BCS vedg6 \ entries in the verge buffer, so jump to vedg6 to \ return from the subroutine LDA vergeDataRight,Y \ If bit 7 is set in vergeDataRight for the new segment BMI vedg1 \ from the track segment list, then this segment is \ hidden behind a hill, so jump to vedg1 move on to the \ next segment LDA prevPitchIndex \ If prevPitchIndex < vergeDepthOfField, then the new CMP vergeDepthOfField \ segment is further away from the player than the depth BCC vedg2 \ of field index in vergeDepthOfField, so jump to vedg2 \ to draw the track edges without verge marks BNE vedg3 \ If prevPitchIndex <> vergeDepthOfField, which means \ prevPitchIndex > vergeDepthOfField, jump to vedg3 \ If we get here then prevPitchIndex = vergeDepthOfField \ so the current segment is right at the point where we \ stop drawing verge marks LDA vergeDataRight-1,Y \ Set A to the colour of the previous entry in the verge AND #3 \ buffer, which is stored in bits 0-2 of vergeDataRight \ (the -1 points to the previous entry, which will be \ one step further away from the player) \ \ The colour values are: \ \ * 0 = black \ * 1 = red \ * 2 = white BNE vedg4 \ If the verge of the previous entry is red or white, \ jump to vedg4 to set A = pixelMaskVerge + A * 4 and \ draw the edge \ If we get here then the verge of the previous entry \ is black STY vergeDepthOfField \ Set Y = vergeDepthOfField SEC \ Set the C flag to pass to DrawSegmentEdge so it does \ not draw the edge LDA vergeType \ Set A to the type of verge we are drawing BEQ vedg5 \ If we are drawing leftVergeStart, jump to vedg5 with \ the C flag set and A = 0 to draw the edge .vedg2 \ If we get here then one of these is true: \ \ * prevPitchIndex < vergeDepthOfField, so the current \ segment is further away from the player than the \ depth of field index in vergeDepthOfField, so we \ draw all verge marks as black \ \ * prevPitchIndex = vergeDepthOfField and the \ previous segment's verge edge is red or white and \ we are not drawing leftVergeStart, so we want to \ draw a black verge mark \ \ In both cases we want to draw black verge marks LDA pixelMaskNoVerge \ Set A = pixelMaskNoVerge \ \ pixelMaskNoVerge always points to a pixel byte that's \ green and black, so is this how we draw the track for \ segments that are beyond the verge depth of field CLC \ Set the C flag to pass to DrawSegmentEdge so it draws \ the edge BCC vedg5 \ Jump to vedg5 with the C flag clear and A set to \ pixelMaskNoVerge to draw the edge (this BCC is \ effectively a JMP as the C flag is always clear) .vedg3 \ If we get here then prevPitchIndex > vergeDepthOfField \ so the current segment is closer to the player than \ the depth of field index in vergeDepthOfField, so we \ draw the verge mark LDA vergeDataRight-1,Y \ Set A to the colour of the previous entry in the verge AND #3 \ buffer, which is stored in bits 0-2 of vergeDataRight \ (the -1 points to the previous entry, which will be \ one step further away from the player) \ \ The colour values are: \ \ * 0 = black \ * 1 = red \ * 2 = white BNE vedg4 \ If the verge of the previous entry is red or white, \ jump to vedg4 to set: \ \ * A = pixelMaskVerge + 4 for red \ \ * A = pixelMaskVerge + 8 for white \ \ And then pass this to DrawSegmentEdge LDA vergeType \ Set A to the type of verge we are drawing CMP #1 \ If we are drawing leftTrackStart, jump to vedg5 with BEQ vedg5 \ the C flag set and A = 1, so it doesn't draw the edge \ but just sets up the variables CMP #2 \ If we are drawing rightVergeStart, jump to vedg5 with BEQ vedg5 \ the C flag set and A = 2, so it doesn't draw the edge \ but just sets up the variables LDA #0 \ Set A = 0, so we pass A = pixelMaskVerge to \ DrawSegmentEdge .vedg4 ASL A \ Set A = pixelMaskVerge + A * 4 ASL A \ CLC \ This also clears the C flag, so the call to ADC pixelMaskVerge \ DrawSegmentEdge draws the verge edge .vedg5 JSR DrawSegmentEdge \ Draw the verge edge for this entry (i.e. this segment) \ in the verge buffer STY prevPitchIndex \ Update the value of prevPitchIndex to point to the \ entry we just processed, which is now the previous \ entry STX prevYawIndex \ Update the value of prevYawIndex to point to the entry \ we just processed, which is now the previous entry JMP vedg1 \ Loop back to vedg1 to process the next entry .vedg6 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: DrawTrack \ Type: Subroutine \ Category: Drawing the track \ Summary: Draw the track into the screen buffer \ Deep dive: Drawing the track verges \ \ ****************************************************************************** .DrawTrack LDA #&80 \ Set P = &80, to use as the low byte of (Q P) in the STA P \ DrawVergeEdge that we call below LDA horizonListIndex \ Set A = X = horizonListIndex + 40 CLC \ ADC #40 \ So X is the index within the track verge buffer of the TAX \ horizon line's left verge (adding 40 moves the index \ from xVergeRightHi to xVergeLeftHi, which are 40 bytes \ apart in memory) CMP #49 \ If A >= 49, jump to dtra1 BCS dtra1 LDA #49 \ Set A = 49, so A has a minimum value of 49 .dtra1 STA vergeDepthOfField \ Set vergeDepthOfField = A \ = max(49, horizonListIndex + 40) \ = max(9, horizonListIndex) + 40 \ \ This sets the depth of field for displaying verge \ marks to at least entry 9 in the verge buffer, which \ is past all the sections (0 to 5) and past the first \ three segments (6 to 8) \ \ Adding 40 then points the index to the left verge LDA #LO(leftSegment) \ Set A to the low byte of leftSegment, so the call to \ MapSegmentsToLines populates the leftSegment table \ The following set R and MM to zero, to use as the low \ bytes in (S R) and (NN MM), though this is only \ because LO(leftSegment) happens to be zero; moving \ leftSegment off a page boundary would break this code STA R \ Set R = 0 to use as the low byte in (S R) STA MM \ Set MM = 0 to use as the low byte in (NN MM) LDY segmentListPointer \ Set Y to index of the last entry in the track segment \ list for the left side of the track JSR MapSegmentsToLines \ Populate the leftSegment table with a mapping of track \ lines on-screen to the verge buffer for the left side \ of the track LDY #0 \ Set Y = 0, so the call to DrawVergeEdge draws the \ leftVergeStart verge (the left edge of the left verge) STY pixelMaskVerge \ Set pixelMaskVerge = 0 LDA vergeDepthOfField \ Set A = vergeDepthOfField, so we only draw the next \ verge for segments that are within the verge depth of \ field JSR DrawVergeEdge \ Draw the left edge of the left verge (leftVergeStart) LDA #8 \ Set pixelMaskVerge = 8 STA pixelMaskVerge LDY #0 \ Set pixelMaskNoVerge = 0 STY pixelMaskNoVerge INY \ Set Y = 1, so the call to DrawVergeEdge draws the \ leftTrackStart verge (the right edge of the left \ verge) LDA horizonListIndex \ Set A = horizonListIndex + 40, so we draw the next CLC \ verge all the way from the horizon to the player ADC #40 JSR DrawVergeEdge \ Draw the right edge of the left verge (leftTrackStart) LDA vergeDepthOfField \ Set A = vergeDepthOfField to pass to the \ SetVergeBackground routine LDX #%00000100 \ Set X = %00000100 to pass to SetVergeBackground as a \ bit mask to use when calculating the background \ colours JSR SetVergeBackground \ Update the background colour table for any verges that \ overlap the left edge of the screen STY vergeTopLeft \ SetVergeBackground sets Y to the track line just above \ the segment at vergeDepthOfField (i.e. the furthest \ segment that might contain a verge), so store this in \ vergeTopLeft LDA horizonListIndex \ Set A = horizonListIndex TAX \ Set X = horizonListIndex CMP #9 \ If A >= 9, jump to dtra1 BCS dtra2 LDA #9 \ Set A = 9, so A has a minimum value of 9 .dtra2 STA vergeDepthOfField \ Set vergeDepthOfField = A \ = max(9, horizonListIndex) + 40 \ \ This sets the depth of field for displaying verge \ marks to at least entry 9 in the verge buffer, which \ is past all the sections (0 to 5) and past the first \ three segments (6 to 8) LDX horizonListIndex \ Set X = horizonListIndex \ \ So X is the index in the track verge buffer for the \ horizon line's right verge LDA #LO(rightSegment) \ Set A to the low byte of rightSegment, so the call to \ MapSegmentsToLines populates the rightSegment table LDY segmentListRight \ Set Y to index of the last entry in the track segment \ list for the right side of the track JSR MapSegmentsToLines \ Populate the rightSegment table with a mapping of \ track lines on-screen to the verge buffer for the \ right side of the track LDA #28 \ Set pixelMaskNoVerge = 28 STA pixelMaskNoVerge LDA #16 \ Set pixelMaskVerge = 16 STA pixelMaskVerge LDY #2 \ Set Y = 2, so the call to DrawVergeEdge draws the \ rightVergeStart verge (the left edge of the right \ verge) LDA horizonListIndex \ Set A = horizonListIndex so we draw the next verge all \ the way from the horizon to the player JSR DrawVergeEdge \ Draw the left edge of the right verge \ (rightVergeStart) LDA #28 \ Set pixelMaskVerge = 28 STA pixelMaskVerge LDY #3 \ Set Y = 3, so the call to DrawVergeEdge draws the \ rightGrassStart verge (the right edge of the right \ verge) LDA vergeDepthOfField \ Set A = vergeDepthOfField, so we only draw the next \ verge for segments that are within the verge depth of \ field JSR DrawVergeEdge \ Draw the right edge of the right verge \ (rightGrassStart) LDA vergeDepthOfField \ Set A = vergeDepthOfField to pass to the \ SetVergeBackground routine LDX #%00010100 \ Set X = %00010100 to pass to SetVergeBackground as a \ bit mask to use when calculating the background \ colours JSR SetVergeBackground \ Update the background colour table for any verges that \ overlap the left edge of the screen STY vergeTopRight \ SetVergeBackground sets Y to the track line just above \ the segment at vergeDepthOfField (i.e. the furthest \ segment that might contain a verge), so store this in \ vergeTopRight RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: SetVergeBackground \ Type: Subroutine \ Category: Drawing the track \ Summary: Update the background colour table for any verges that overlap the \ left edge of the screen \ Deep dive: Drawing the track verges \ \ ------------------------------------------------------------------------------ \ \ This routine works through any track lines that might contain a verge, and \ checks whether the verge crosses the left edge of the screen, updating the \ relevant entry in the background colour table if it does. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ A Index of the first entry to check in the verge buffer, \ typically set to vergeDepthOfField so only entries that \ might contain a verge are checked \ \ X Set to a bit mask as follows: \ \ * %000 00 1 00 (after drawing the left verge) \ \ * %000 10 1 00 (after drawing the right verge) \ \ Only bits 2-4 of this are used \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ Y The track line just above the pitch angle of the segment \ at vergeDepthOfField (i.e. the furthest segment that \ might contain a verge) \ \ ****************************************************************************** .SetVergeBackground STX GG \ Set GG to the value passed in X STA U \ Set U to the index into the verge buffer DEC vergeBufferEnd \ Decrement the index of the last entry in the track \ verge buffer for the verge we are drawing LDA playerSideways \ If playerSideways < 40, then the player's car is CMP #40 \ facing along the track rather than sideways, so jump BCC sver8 \ to sver8 to join the loop below BCS sver9 \ If we get here then the player's car is facing \ sideways relative to the track direction, so jump to \ sver9 to skip the loop (this BCS is effectively a JMP \ as we just passed through a BCC) \ The following loop uses X as a loop counter to work \ through the verge buffer from entry U to the end, \ setting the background colour if the verge goes off \ the left of the screen .sver1 LDY yVergeRight,X \ If the pitch angle (i.e. the track line) of the X-th CPY #80 \ entry in the verge buffer is 80 or more, then it is BCS sver7 \ not on-screen, so jump to sver7 to move on to the next \ entry in the verge buffer LDA vergeDataRight,X \ If bit 7 of the X-th entry's vergeDataRight is set, BMI sver7 \ then this segment is hidden behind a hill, so jump to \ sver7 to move on to the next entry in the verge buffer LDA GG \ If GG = %00010100, which is the value we passed after CMP #%00010100 \ drawing the right verge, then jump to sver2 BEQ sver2 LDA xVergeRightHi+16,X \ Set W to the high byte of the yaw angle of the verge's STA W \ outer edge LDA xVergeRightHi,X \ Set A to the high byte of the yaw angle of the verge's \ inner edge JMP sver3 \ Jump to sver3 to keep going .sver2 LDA xVergeRightHi,X \ Set W to the high byte of the yaw angle of the verge's STA W \ inner edge LDA xVergeRightHi+16,X \ Set A to the high byte of the yaw angle of the verge's \ outer edge .sver3 \ At this point, W is the yaw angle of the leftmost \ verge edge, and A is the yaw angle of the rightmost \ verge edge, for the verge that we are drawing CLC \ Set A = A + 20 ADC #20 BMI sver7 \ If A is negative, jump to sver7 to move on to the next \ entry in the verge buffer LDA W \ Set A = W + 20 CLC ADC #20 BPL sver7 \ If A is positive, jump to sver7 to move on to the next \ entry in the verge buffer \ If we get to this point, then: \ \ * A + 20 is positive \ \ * W + 20 is negative \ \ Adding 20 degrees to the yaw angles will move them to \ the right by half the screen width, so this is the \ same as moving the angles from the left edge of the \ screen to the middle \ \ We then check whether moving the angles to the centre \ pushes the rightmost verge edge in A past the centre \ (i.e. positive), while still leaving the leftmost edge \ in the left half (i.e. negative) \ \ If so, then this means the verge is straddling the \ left edge of the screen, so we need to consider \ setting the background colour for this track line to \ the verge colour \ First, though, we need to skip forward through the \ verge buffer until we get to the next entry that is \ followed by a non-hidden entry (so this skips over \ any entries that are hidden behind hills) \ \ This ensures that we get to an entry in the verge \ buffer that has a verge colour, as hidden entries do \ not have a colour associated with them .sver4 LDA vergeDataRight+1,X \ If the next entry in the verge buffer is not hidden BPL sver5 \ behind a hill, jump to sver5 INX \ Increment X to point to the next entry in the verge \ buffer INC U \ Increment U to point to the next entry in the verge \ buffer CPX vergeBufferEnd \ If X < vergeBufferEnd then we haven't reached the end BCC sver4 \ of the verge buffer, so loop back to check the next \ entry .sver5 LDA backgroundColour,Y \ Set T to the current entry in the background colour STA T \ table for track line Y LDA backgroundColour,Y \ Set A to the current entry in the background colour \ table for track line Y BEQ sver6 \ If A = 0, then the background colour is currently \ set to black, so jump to sver6 to set the background \ colour to the verge colour AND #%00011100 \ Extract bits 2-4 of A, which contain the verge type in \ bits 3-4, and details in bit 2 of whether the colour \ was set by the SetVergeBackground routine CMP GG \ If A = GG, then bits 2-4 match the mask we passed to BEQ sver6 \ the routine, so jump to sver6 to set the background \ colour to the verge colour \ \ The possible values of GG are: \ \ * %000 00 1 00 (after drawing the left verge) \ \ * %000 10 1 00 (after drawing the right verge) \ \ So this jumps if we already set the background colour \ entry in this routine, and the verge type was \ leftVergeStart after drawing the left verge, or \ rightVergeStart after drawing the right verge \ If we get here then bits 2-4 of the current background \ colour do not match the mask in GG, so we have not \ already set this colour on this routine for the \ leftVergeStart or rightVergeStart verges ROR A \ Rotate the C flag into bit 7 of A, where: \ \ * C flag is clear if A < GG \ \ * C flag is set if A > GG EOR T \ If T and A have different values of bit 7, jump to BMI sver7 \ sver7 to move on to the next entry in the verge buffer \ Otherwise we fall through into sver6 to set the \ background colour to the verge colour .sver6 LDA vergeDataRight,X \ Set A to the verge data for the entry in the verge \ buffer that is crossing the left edge of the screen AND #%00000011 \ Extract the colour of the verge, which is in bits 0-1, \ so this is the colour that we want to store in the \ background colour table (as this is the colour of the \ verge mark that's at the left edge of the screen) ORA GG \ OR the verge colour with the mask that we passed to \ the routine, to give: \ \ * %000001xx (after drawing the left verge) \ \ * %000101xx (after drawing the right verge) \ \ where %xx is the colour of the verge STA backgroundColour,Y \ Store the result as the background colour for track \ line Y .sver7 INC U \ Increment the verge buffer index in U to move on to \ the next entry in the verge buffer .sver8 \ This is where we join the loop LDX U \ Set X to the loop counter in U, which contains the \ index of the entry to process in the verge buffer CPX vergeBufferEnd \ If X < vergeBufferEnd, jump back to sver1 to process BCC sver1 \ this entry .sver9 LDX vergeDepthOfField \ Set Y to the pitch angle (i.e. the track line) of the LDY yVergeRight,X \ entry in the verge buffer at the depth of field INY \ Increment the track line in Y RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: DrawCornerMarkers \ Type: Subroutine \ Category: Drawing objects \ Summary: Draw any visible corner markers \ Deep dive: Corner markers \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ markersToDraw The number of corner markers to draw - 1 \ \ ****************************************************************************** .DrawCornerMarkers LDY #0 \ We work our way through the markers we need to draw, \ using Y as the marker number, counting up from 0 to \ markersToDraw - 1 .corn1 CPY markersToDraw \ If Y = markersToDraw, then we have drawn all the BEQ corn7 \ markers, so jump to corn7 to reset markersToDraw to \ zero and return from the subroutine LDX markerListIndex,Y \ Set X = markerListIndex for marker Y, so X contains \ the index of the corresponding entry in the track \ segment list for this corner markers STY markerNumber \ Store the marker number in markerNumber, so we can \ retrieve it at the end of the loop LDA markerData,Y \ If bit 5 of markerData for marker Y is clear, then the AND #%00100000 \ marker is white, so jump to corn2 to skip the BEQ corn2 \ following two instructions LDA #%00001111 \ Map logical colour 2 in the colour palette to physical STA colourPalette+2 \ colour 1 (red in the track view), so the corner marker \ is drawn in red rather than white .corn2 LDA xMarkerHi,Y \ Set (U A) = (xMarkerHi xMarkerLo) for marker Y STA U \ LDA xMarkerLo,Y \ So (U A) contains the x-axis distance between the \ track edge and the marker ASL A \ Set (U A) = (U A) << 1 ROL U \ = xMarker * 2 STA T \ Set (U T) = (U A) \ = xMarker * 2 CLC \ Set (A V) = (U A) + X-th value from xVergeRight ADC xVergeRightLo,X \ STA V \ starting with the low bytes LDA xVergeRightHi,X \ And then the high bytes ADC U \ \ So (A V) contains the verge's x-coordinate plus the \ distance between the track edge and the marker \ (doubled), to give the x-coordinate of the marker CMP #24 \ If A < 24, jump to corn3 BCC corn3 CMP #232 \ If A < 232, i.e. 24 <= A < 232, jump to corn6 to move BCC corn6 \ on to the next loop .corn3 \ If we get here then A < 24 or A >= -24, so the marker \ is on-screen ASL V \ Set (A V) = (A V) << 2 ROL A \ ASL V \ so A < 96 or A >= -96 ROL A CLC \ Set xPixelCoord = A + 80 ADC #80 \ STA xPixelCoord \ where 80 is the x-coordinate of the middle of the \ screen (as the screen is 160 pixels wide) LDA yVergeRight,X \ Set yPixelCoord = X-th value from yVergeRight STA yPixelCoord \ \ Which is the pitch angle (i.e. y-coordinate) of \ the verge that has the corner markers LDY #2 \ Set Y = 2 so the following loop shifts (U T) left by \ two places .corn4 ASL T \ Set (U T) = (U T) << 1 ROL U DEY \ Decrement the shift counter BNE corn4 \ Loop back until we have left-shifted by Y places LDA U \ Set A = U, so we now have: \ \ (A T) = (U T) << 2 \ = xMarker * 2 * 2 BPL corn5 \ If A is positive, jump to corn5 to skip the following EOR #&FF \ A is negative, so negate A using two's complement, so CLC \ A now contains |A| ADC #1 .corn5 STA scaleUp \ Set scaleUp = |A| \ \ So the size of the corner marker is based on the \ x-axis distance between the track edge and the marker LDA #6 \ Set objectType = 6, the object type for a corner STA objectType \ marker JSR DrawObject \ Draw the corner marker .corn6 LDA #%11110000 \ Map logical colour 2 in the colour palette to physical STA colourPalette+2 \ colour 1 (white in the track view), which sets it back \ to the default value LDY markerNumber \ Set Y to the marker number that we stored in \ markerNumber at the start of the loop INY \ Increment the marker number to draw the next loop JMP corn1 \ Loop back to corn1 .corn7 LDA #0 \ Reset markersToDraw to zero as we have drawn all the STA markersToDraw \ corner markers RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: UpdatePositionInfo \ Type: Subroutine \ Category: Text \ Summary: Apply any position changes and update the position information at \ the top of the screen \ \ ****************************************************************************** .UpdatePositionInfo LDA positionChangeBCD \ Set A = positionChangeBCD BEQ posi1 \ If A = 0 then the race position has not changed, so \ jump to posi1 to skip updating the position number \ Otherwise we need to add the position change to the \ current position number, so we can update the number \ at the top of the screen SED \ Set the D flag to switch arithmetic to Binary Coded \ Decimal (BCD) CLC \ Set A = currentPositionBCD + A ADC currentPositionBCD \ = currentPositionBCD + positionChangeBCD STA currentPositionBCD \ Set currentPositionBCD = A CLD \ Clear the D flag to switch arithmetic to normal BEQ posi1 \ If A = 0, jump to posi1 CMP #&21 \ If A >= &21, jump to posi1 BCS posi1 LDX #0 \ Set positionChangeBCD = 0, as we have now applied the STX positionChangeBCD \ change of position to currentPositionBCD STX G \ Set G = 0 so the call to Print2DigitBCD below will \ print the second digit and will not print leading \ zeroes when printing the position number LDX #10 \ Print the position number in A at column 10, pixel LDY #24 \ row 24, on the first text line at the top of the JSR Print2DigitBCD-6 \ screen .posi1 BIT updateDriverInfo \ If bit 7 of updateDriverInfo is clear, jump to posi2 BPL posi2 \ to skip printing the driver names at the top of the \ screen LDY positionAhead \ Set Y to the position of the driver in front of us LDA #24 \ Print the name of driver Y in the "In front:" part of JSR PrintNearestDriver \ the header LDY positionBehind \ Set Y to the position of the driver behind us LDA #33 \ Print the name of driver Y in the "Behind:" part of JSR PrintNearestDriver \ the header .posi2 LSR updateDriverInfo \ Clear bit 7 of updateDriverInfo so we don't update the \ driver names until the value of updateDriverInfo \ changes RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: ProcessContact \ Type: Subroutine \ Category: Car geometry \ Summary: Process collisions between the player and the other cars \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ V Bit 7 is set if there is a collision \ \ ****************************************************************************** .ProcessContact LDA processContact \ If processContact is zero then there no other cars BEQ DrawObjectEdge-1 \ close enough to the player's car for there to be any \ contact, so return from the subroutine (as \ DrawObjectEdge-1 contains an RTS) LDA #0 \ Set processContact = 0 to reset the flag, so we only STA processContact \ check for contact when a car is flagged as being close SEC \ Set bit 7 of V ROR V LDA #37 \ Set A = 37 - objectDistanceLo SEC \ SBC objectDistanceLo \ The value of (objectDistanceHi objectDistanceLo) is \ left over from the last call to CheckForContact, which \ was last called for the nearest car in front of us \ when building object for the five cars in front of us, \ from furthest to nearest, as part of this chain of \ routines: \ \ MoveAndDrawCars > BuildVisibleCar > BuildCarObjects \ > GetObjectAngles > CheckForContact \ \ Note that BuildVisibleCar does get called once more at \ the end of MoveAndDrawCars, for the car behind us, but \ as that car is not visible, it doesn't set the object \ distance \ \ So, in short, objectDistanceLo is the low byte of the \ distance between the player and the nearest car, which \ is the car we want to process for a collision \ \ This means that a higher value of A means a closer \ collision, so this is effectively a measure of how \ dangerous this collision is, in the range 0 to 37 BCS cont1 \ If the above subtraction didn't underflow then the \ other car is at a distance of 37 or less, so jump to \ cont1 to skip the following instruction LDA #5 \ The subtraction underflowed, so the other car is a bit \ of a distance away, so set A = 5 so have a minor \ collision .cont1 ASL A \ Set A = A * 2 \ \ so the damage measure is now in the range 0 to 74 STA U \ Set U = A, so U now contains the damage measure in the \ range 0 to 74 LDX collisionDriver \ Set X to the driver number of the car being hit (the \ "other car") LDY currentPlayer \ Set Y to the driver number of the current player CMP #40 \ If A < 40, then the collision is not bad enough to BCC cont2 \ push the other car off the track, so jump to cont2 LDA raceStarted \ If bit 7 of raceStarted is clear then this is either BPL cont2 \ a practice or qualifying lap, so jump to cont2 to skip \ the following instruction JSR PushCarOffTrack \ If we get here then the collision is close enough to \ push the other car off the track, and this is a race, \ so we push the other car off the track and out of the \ race .cont2 LDA objYawAngleHi,X \ Set A to the yaw angle for the other car minus the yaw SEC \ angle for the player, which we will call dYawAngle SBC playerYawAngleHi ASL A \ Set A = A * 4 ASL A \ = dYawAngle * 4 PHP \ Push the N flag onto the stack, which contains the \ sign of dYawAngle LDA carSpeedHi,Y \ Set A to the high byte of the player's speed CPX #20 \ If the driver number of the other car is >= 20, then BCS cont4 \ jump to cont4 as this is not a computer-controlled \ driver, so we do not adjust its speed CMP carSpeedHi,X \ If the high byte of the player's speed is >= the high BCS cont3 \ byte of the other driver's speed, jump to cont3 with \ A containing the higher of the two speeds, so the \ other car gets bumped to a slightly higher speed than \ the faster car (by adding 12, as the C flag is set) LDA carSpeedHi,X \ Set A to the high byte of the other driver's speed, so \ A now contains the higher of the two speeds BNE cont4 \ Jump to cont4 to leave the speed of the other car \ alone, as it is going faster than the player (this BNE \ is effectively a JMP, as we know from the above that \ carSpeedHi,Y < carSpeedHi,X, which implies that the \ value of carSpeedHi,X must be non-zero) .cont3 \ If we get here, it's because the player is going \ faster than the other car, and we jumped here via a \ BCS, so the C flag is set, which means the following \ adds 12 to the other car's speed ADC #11 \ Increase the high byte of the other driver's speed by STA carSpeedHi,X \ 12, to speed it up after being hit by the faster \ player's car .cont4 \ By this point, A contains the speed of the faster car \ following the collision, as the high byte of the speed JSR Multiply8x8 \ Set (A T) = A * U \ = carSpeedHi * damage measurement (0 to 74) \ \ So (A T) is higher with closer and faster collisions CMP #16 \ If A < 16, jump to cont5 to skip the following BCC cont5 LDA #16 \ A >= 16, so set A to 16 as the maximum value of A .cont5 \ By this point, (A T) is a measurement of how dangerous \ the collision was, on a scale of 0 to 16, so now we \ convert that into the amount of spin to apply to our \ car PLP \ Restore the sign of dYawAngle, which we stored on the \ stack above, so the N flag is positive if the other \ car's yaw angle is larger (i.e. the other car is to \ the right of the player's car), or negative if the \ other car's yaw angle is smaller (i.e. the other car \ is to the left of the player's car) JSR Absolute16Bit \ Set the sign of (A T) to match the result of the \ subtraction above, so A is now in the range -16 to \ +16, with the sign reflecting the position of the \ other car: \ \ * -16 to 0 if the other car is to the left \ \ * 0 to +16 if the other car is to the right \ \ So we can use this to apply spin to our car, so we \ spin in the correct direction and with an amount \ that's proportional to the severity of the collision \ Fall through into SquealTyres to set spinYawAngleTop \ to the amount of yaw spin in A and make the sound of \ squealing tyres \ ****************************************************************************** \ \ Name: SquealTyres \ Type: Subroutine \ Category: Driving model \ Summary: Make the tyres squeal \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ A The new value for spinYawAngleTop \ \ ****************************************************************************** .SquealTyres STA spinYawAngleTop \ Set spinYawAngleTop = A LDA #%10000000 \ Set bit 7 in tyreSqueal for both tyres, so they squeal STA tyreSqueal STA tyreSqueal+1 LDA #4 \ Make sound #4 (crash/contact) at the current volume JSR MakeSound-3 \ level RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: DrawObjectEdge (Part 1 of 5) \ Type: Subroutine \ Category: Drawing objects \ Summary: Draw the specified edge of an object part \ Deep dive: Creating objects from edges \ \ ------------------------------------------------------------------------------ \ \ This part of the routine calculates the pixel x-coordinate of the edge, plus \ the block number and pixel x-coordinate of the next edge (if there is one), so \ we can use them in the next call to DrawObjectEdge. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ topTrackLine Top track line of the edge (higher value, 0 to 79) \ \ bottomTrackLine Bottom track line of the edge (lower value, 0 to 79) \ \ thisEdge This edge (as a scaled scaffold measurement) \ \ nextEdge The next edge (as a scaled scaffold measurement) \ \ xPixelCoord The pixel x-coordinate of the centre of the object \ \ colourData Colour data: \ \ * Bits 0-1 = logical fill colour \ \ * Bits 2-3 = logical edge colour \ \ * Bit 4 = if set and this is a left or right edge, \ then use the fill colour instead of the edge colour \ if this is an outside edge (i.e. a left edge in the \ left half of the screen, or a right edge in the \ right half of the screen), so the edge is \ effectively hidden \ \ A The fill colour to the right of the edge to draw: \ \ * For left edges: bits 0-1 contain the fill colour \ from bits 0-1 of the colour data \ \ * For right edges: contains 0 \ \ * For extra edges: bits 0-1 contain the fill colour \ from bits 0-1 of the colour data \ \ Y Edge type: \ \ * 0 = second or third edge in a four-edge object part \ (an "extra edge") \ \ * 1 = left edge \ \ * 2 = right edge \ \ rightOfEdge The fill byte to the right of the previous edge (or, if \ this is the first edge, the background colour to the \ left of the first edge) \ \ If this is a second call to DrawObjectEdge and we need \ to draw this edge in the same pixel byte as the previous \ edge, this contains the pixel byte from the previous \ call with the first edge already drawn \ \ blockNumber For extra or right edges only: the dash data block \ number of the previous edge drawn by DrawObjectEdge \ \ nextEdgeCoord For extra or right edges only: the pixel x-coordinate \ of the edge to draw (as returned by the previous call to \ DrawObjectEdge) \ \ nextBlockNumber For extra or right edges only: the data block number of \ the edge to draw (returned by the previous call to \ DrawObjectEdge) \ \ prevEdgeInByte Determines whether we have already inserted the previous \ edge into the pixel byte we are building \ \ * 0 = there is no other edge in the pixel byte we are \ building \ \ * Non-zero = the previous edge is already in the pixel \ byte we are building, and the current \ edge needs to go in the same byte \ \ edgePixelMask For extra or right edges only: \ \ * 0 = there is no other edge in the pixel byte we are \ building \ \ * Non-zero = the pixel mask of the edge that was drawn \ into rightOfEdge in the previous call \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ blockNumber The dash data block number that was drawn into \ \ rightOfEdge The fill byte to the right of the edge we just drew: \ \ * If we just successfully drew an edge and drew the \ result on-screen, this contains the fill colour of \ the object \ \ * If we created a pixel byte but the next edge needs \ to be drawn in the same byte, this contains the \ pixel byte from this call \ \ edgePixelMask The pixel mask of the edge that was drawn, which will: \ \ * Be empty if the previous edge was already drawn \ and we do not need to share this pixel byte with \ the previous edge \ \ * Contain set pixels for the previous edge if both \ this edge and the previous edge need to share the \ same pixel byte \ \ nextEdgeCoord The pixel x-coordinate for the next edge \ \ nextBlockNumber The dash data block number for the next edge \ \ prevEdgeInByte The correct setting for the next edge: \ \ * 0 if the next edge is not in the same byte as the \ one we just drew \ \ * Non-zero (bit 7 is set) if the next edge is in the \ same byte as the one we just drew (in which case the \ screen has not been updated, and the next call to \ DrawObjectEdge needs to insert the next edge into \ the pixel byte in rightOfEdge, using the pixel \ mask in edgePixelMask) \ \ ------------------------------------------------------------------------------ \ \ Other entry points: \ \ DrawObjectEdge-1 Contains an RTS \ \ ****************************************************************************** .DrawObjectEdge STY J \ Set J to the edge type in Y, so we can fetch it later LDX rightOfEdge \ Set leftOfEdge = rightOfEdge, so we effectively step STX leftOfEdge \ along the line, from the previous edge to this edge \ (so the fill byte to the right of the previous edge \ becomes the fill byte to the left of this edge) AND #3 \ Set X to bits 0-1 of A, which contains the logical TAX \ colour of the object's fill colour LDA objectPalette,X \ Set rightOfEdge to logical colour X from the object STA rightOfEdge \ palette, so we fill the object to the right of this \ edge with the colour in A LDA blockNumber \ Set prevBlockNumber to the dash data block number that STA prevBlockNumber \ was left over from the previous call to DrawObjectEdge \ so we can use it if this isn't a left edge LDA colourData \ Set X to bits 2-3 of colourData, which contains the AND #%00001100 \ logical colour of the edge we want to draw LSR A LSR A TAX LDA objectPalette,X \ Set edgePixel to logical colour X from the object STA edgePixel \ palette, which we will mask later to contain just the \ single pixel required to draw the edge LDA #0 \ Set P = 0, for use as the low byte of (Q P), in which STA P \ we are going to build the address we need to draw into \ in the dash data \ We now set thisEdge and blockNumber according to the \ edge type: \ \ * Left edge, set: \ \ thisEdge = xPixelCoord + thisEdge / 2 \ blockNumber = thisEdge / 4 \ \ * Right or extra edge, set: \ \ thisEdge = nextEdgeCoord \ blockNumber = blockNumberForNext CPY #1 \ If Y <> 1, jump to draw3 BNE draw3 \ If we get here then Y = 1, so we are drawing the left \ edge LDA thisEdge \ Set A to the scaled scaffold measurement for the left \ edge, which was passed to the routine in thisEdge \ We now set A = A / 2, retaining the sign in A and \ rounding towards zero BPL draw1 \ If A is positive, jump to draw1 SEC \ Set A = A / 2, inserting a set bit into bit 7 to ROR A \ retain the sign of A, and rounding the division up ADC #0 \ towards zero by adding bit 0 of A to the result JMP draw2 \ Jump to draw2 to skip the following instruction .draw1 LSR A \ Set A = A / 2, which will retain the sign of A as we \ know A is positive, rounding the result down towards \ zero .draw2 CLC \ Set thisEdge = A + xPixelCoord ADC xPixelCoord \ = thisEdge / 2 + xPixelCoord STA thisEdge LSR A \ Set blockNumber = A / 4 LSR A \ = thisEdge / 4 STA blockNumber JMP draw4 \ Jump to draw4 .draw3 \ We jump here if Y <> 1, i.e. Y = 0 or 2, so we are \ either drawing an extra edge or the right edge LDA nextBlockNumber \ Set blockNumber = nextBlockNumber STA blockNumber LDA nextEdgeCoord \ Set thisEdge = nextEdgeCoord STA thisEdge CPY #0 \ If Y <> 0, jump to draw7 BNE draw7 .draw4 \ We have now set thisEdge and blockNumber according to \ the edge type, so now we set nextEdgeCoord and \ nextBlockNumber as follows: \ \ nextEdgeCoord = xPixelCoord + nextEdge / 2 \ \ nextBlockNumber = nextEdgeCoord / 4 LDA nextEdge \ Set A to the scaled scaffold measurement for the next \ edge \ We now set A = A / 2, retaining the sign in A and \ rounding towards zero BPL draw5 \ If A is positive, jump to draw5 SEC \ Set A = A / 2, inserting a set bit into bit 7 to ROR A \ retain the sign of A, and rounding the division up ADC #0 \ towards zero by adding bit 0 of A to the result JMP draw6 \ Jump to draw6 to skip the following instruction .draw5 LSR A \ Set A = A / 2, which will retain the sign of A as we \ know A is positive, rounding the result down towards \ zero .draw6 CLC \ Set nextEdgeCoord = A + xPixelCoord ADC xPixelCoord \ = nextEdge / 2 + xPixelCoord STA nextEdgeCoord LSR A \ Set nextBlockNumber = A / 4 LSR A \ = nextEdgeCoord / 4 STA nextBlockNumber \ By this point we have: \ \ * Left edge: \ \ thisEdge = xPixelCoord + thisEdge / 2 \ blockNumber = thisEdge / 4 \ \ * Right or extra edge, set: \ \ thisEdge = nextEdgeCoord \ blockNumber = blockNumberForNext \ \ and we also have the following: \ \ nextEdgeCoord = xPixelCoord + nextEdge / 2 \ nextBlockNumber = nextEdgeCoord / 4 \ \ So we have: \ \ * leftOfEdge contains the fill colour of the object, \ or if the previous edge is within the same pixel \ byte and we are now drawing the next edge, it \ contains the pixel byte from the previous call, \ which contains the previous edge \ \ * rightOfEdge contains the fill colour, or (if we \ need to draw this edge in the same pixel byte as \ the previous edge), it contains the pixel byte \ from the previous call \ \ * edgePixel contains a four-pixel byte in the edge \ colour passed in bits 2-3 of colourData, which we \ will mask later to a single pixel \ \ * thisEdge contains the pixel x-coordinate of the \ edge to draw \ \ * blockNumber contains the dash data block number \ for the edge to draw (as each dash data block is \ four pixels wide) \ \ * nextEdgeCoord and nextBlockNumber contain the \ pixel x-coordinate and dash data block number of \ the next edge, ready to be used in the next call \ to DrawObjectEdge \ ****************************************************************************** \ \ Name: DrawObjectEdge (Part 2 of 5) \ Type: Subroutine \ Category: Drawing objects \ Summary: Calculate the screen address for the edge we want to draw \ Deep dive: Creating objects from edges \ \ ****************************************************************************** .draw7 LDA blockNumber \ Set A to the dash data block number for the edge to \ draw, which we stored in blockNumber in part 1 CMP #20 \ If blockNumber >= 20, set bit 0 of T, otherwise clear ROL T \ it, so bit 0 of T is clear if the edge is in the left \ half of the screen, and set if the edge is in the \ right half LSR A \ Set (A P) = (A P) >> 1 ROR P \ = (blockNumber 0) >> 1 \ = blockNumber * 128 \ = blockNumber * &80 CLC \ Set (Q P) = (A P) + dashData ADC #HI(dashData) \ STA Q \ This addition works because the low byte of dashData \ is zero \ So we now have: \ \ (Q P) = dashData + blockNumber * &80 \ \ which is the start address of the dash data block, \ because the dash data blocks occur every &80 bytes \ from dashData IF _ACORNSOFT OR _4TRACKS STA draw27+2 \ Modify the following instruction at draw27: LDA P \ STA draw27+1 \ LDX &3000,Y -> LDX #(Q P),Y \ \ This is pseudo-code, but it means we have modified the \ instruction to load the Y-th byte from the dash data \ block address we just calculated, i.e. load the Y-th \ byte of the dash data block for the edge to draw ENDIF LDX blockNumber \ Set X to the dash data block number for the edge to \ draw CPX #40 \ If blockNumber < 40 then blockNumber is a valid dash BCC draw8 \ data block number in the range 0 to 39, so jump to \ draw8 to keep going JMP draw32 \ Otherwise blockNumber is not a valid dash data block \ number and is off the right edge of the screen, so \ jump to draw32 to work out whether we need to fill \ the object all the way to the right edge of the screen .draw8 LDA bottomTrackLine \ Set A = bottomTrackLine, which is the number of the \ track line at the bottom of the object, and which is \ the same as the offset into the dash data block for \ the bottom edge CMP dashDataOffset,X \ If A >= the dash data offset for our dash data block, BCS draw9 \ then A is pointing to dash data, so jump to draw9 to \ skip the following instruction LDA dashDataOffset,X \ Set A to the dash data offset for our dash data block, \ so it points to the first byte of the block's dash \ data (i.e. the lowest byte of the dash data block \ on-screen) .draw9 STA blockOffset \ Set blockOffset = A, so blockOffset contains the dash \ data block offset for the bottom track line of the \ edge we want to draw CMP topTrackLine \ If A < topTrackLine, then the track lines are the BCC draw11 \ right way around, so jump to draw11 to keep going in \ part 3 CPY #1 \ If Y <> 1, then we are drawing either a right edge or BNE draw10 \ an extra edge, so jump to draw29 via draw10 to check \ whether we need to fill to the left of this edge, and \ then move on to the next edge \ If we get here then A >= topTrackLine and Y = 1, so \ we are drawing the left edge and the bottom track line \ is higher than the top track line, which means there \ is nothing to draw, so we return from the subroutine RTS \ Return from the subroutine .draw10 JMP draw29 \ Jump to draw29 to check whether we need to fill to the \ left of this edge, and then move on to the next edge \ ****************************************************************************** \ \ Name: DrawObjectEdge (Part 3 of 5) \ Type: Subroutine \ Category: Drawing objects \ Summary: Construct a pixel byte for the edge we want to draw \ Deep dive: Creating objects from edges \ \ ****************************************************************************** .draw11 LDA colourData \ If bit 4 of colourData is clear, jump to draw12 to AND #%00010000 \ keep the edge colour we set at the start of part 1 BEQ draw12 TYA \ If Y = 0, then we are drawing an extra edge, so jump BEQ draw12 \ to draw12 to keep the edge colour we set at the start \ of part 1 \ Otherwise bit 4 of colourData is set and this is a \ left or right edge, which means we use the fill colour \ instead of the edge colour, but only if this is an \ outside edge (i.e. a left edge in the left half of the \ screen, or a right edge in the right half of the \ screen), so the edge is effectively hidden by merging \ it into the object's fill EOR T \ Set A = bit 0 of Y EOR bit 0 of T AND #1 BEQ draw12 \ If A = 0, then one of the following is true: \ \ * Y = 2 (%10) and bit 0 of T = 0, in which case we \ are drawing a right edge in the left half of the \ screen \ \ * Y = 1 (%01) and bit 0 of T = 1, in which case we \ are drawing a left edge in the right half of the \ screen \ \ In either case, jump to draw12 to keep the colour we \ set in edgePixel at the start of part 1, as this is \ not an outside edge \ If we get here then bit 4 of colourData is set, and we \ are drawing a left or right edge as an outside edge, \ so we set the edge colour to the logical colour in \ bits 0-1 of colourData, i.e. the fill colour LDA colourData \ Set X to bits 0-1 of colourData, which contains the AND #%00000011 \ fill colour of the object part we are drawing TAX LDA objectPalette,X \ Set edgePixel to the fill colour, so the edge merges STA edgePixel \ into the object's background .draw12 LDA thisEdge \ Set A to thisEdge, which we set in part 1 to the \ pixel x-coordinate of the edge to draw AND #3 \ Set X = A mod 4, which is the number of the pixel of TAX \ the edge we want to draw within the four-pixel byte \ (i.e. 0 to 3, left to right) LDA yLookupLo+8,X \ Set A to the X-th pixel mask from yLookupLo+8, which \ is a pixel byte with the X-th pixel clear EOR #&FF \ Invert A so it contains a pixel byte with only the \ X-th pixel set AND edgePixel \ Apply the pixel mask to the edge colour in edgePixel, STA edgePixel \ so edgePixel now contains a pixel byte with only the \ X-th pixel set, and that pixel is set to the edge \ colour CPY #1 \ If Y >= 1, then we are drawing a left or right edge, BCS draw16 \ so jump to draw16 \ If we get here then Y = 0, so we are drawing one of \ the extra edges as part of a four-edge object part LDA pixelsToLeft,X \ Set A to the X-th pixel mask from pixelsToLeft, which \ is a pixel byte with all the pixels set to the left of \ the X-th pixel AND leftOfEdge \ Set these pixels to leftOfEdge, which is either the STA T \ fill colour of the object (if there is just this edge \ in the pixel byte), or it's the pixel byte with the \ previous edge already drawn (if we have both edges in \ the same byte) \ \ In either case, this fills the pixel byte with the \ correct contents to the left of this edge and stores \ them in T LDA rightOfEdge \ Set A to a pixel byte with the pixels to the right of AND pixelsEdgeRight,X \ the X-th pixel set to rightOfEdge, so this is the same \ thing but with the bytes to the right of the edge ORA T \ OR the two together so we have a pixel byte with the \ correct bytes to the left and right of this edge AND yLookupLo+8,X \ Apply the X-th pixel mask from yLookupLo+8, so this \ clears the X-th pixel in the pixel byte ORA edgePixel \ We set up edgePixel above to contain a single pixel \ for the edge in position X, set to the edge colour, \ so this sets the X-th pixel in A to the edge colour, \ so the pixel byte in A now contains the edge itself STA I \ A now contains a pixel byte with the correct bytes set \ to the left and right of the edge, plus the correct \ colour set for the edge pixel, so store this pixel \ byte in I LDA prevEdgeInByte \ If prevEdgeInByte = 0 then this edge is the first one BEQ draw13 \ in this byte, so jump to draw13 to potentially draw \ this edge using a quick-draw routine that doesn't \ worry about any existing background content (as this \ is always overwritten by the extra parts in the middle \ of the object part) \ If we get here then prevEdgeInByte is non-zero, so \ this pixel byte contains both the previous edge and \ this edge LDA #0 \ Set prevEdgeInByte = 0, to reset it for the next call STA prevEdgeInByte \ to DrawObjectEdge, as we have moved on to the next \ edge since it was made non-zero LDA edgePixelMask \ Set L to the pixel mask for the previous edge, which STA L \ we set in the previous call to DrawObjectEdge to have \ bits set for the pixels to the right of the previous \ edge, so once the extra edges are done we can use it \ when filling the last part of the extra section EOR #&FF \ Set A to the inverse, so it has bits set for the \ pixels of the previous edge and everything to its left AND I \ Insert the pixels from I so the left part of our pixel \ byte is given the correct edge and everything to the \ left JMP draw17 \ Jump to draw17 to draw the edge using this pixel byte .draw13 \ If we get here then we are not sharing this pixel byte \ with the previous edge, so we can use a quick-draw \ routine that doesn't worry about any existing \ background content (as this is an extra edge) LDX blockNumber \ If blockNumber <> nextBlockNumber, then the next edge CPX nextBlockNumber \ isn't in the same dash data block (i.e. in the same BNE draw14 \ column), so jump to draw14 to draw this edge \ Otherwise the next edge is in the same byte as this \ one, so we need to return from the subroutine via \ draw29 (i.e. first check whether we need to fill to \ the left of this edge, and then return) LDA I \ Store the pixel byte in rightOfEdge so the next call STA rightOfEdge \ to DrawObjectEdge can add the next edge to it JMP draw29 \ Jump to draw29 to check whether we need to fill to the \ left of this edge, and then move on to the next edge .draw14 \ We now draw this edge using a quick-draw routine that \ doesn't worry about any existing background content LDA I \ Set A to the pixel byte for the edge we want to draw BNE draw15 \ If the pixel byte is non-zero, jump to draw15 to skip \ the following instruction LDA #&55 \ Set A = &55 to use as the value of WW below (&55 in \ the screen buffer represents colour 0, or black) .draw15 LDY topTrackLine \ Set Y = topTrackLine, which is the number of the \ track line at the top of the object, which is the \ same as the offset into the dash data block for the \ top edge JMP DrawEdge \ Jump to DrawEdge to draw the edge from topTrackLine \ down to blockOffset, which we set in part 2 to the \ bottom track line of the edge we want to draw, and \ rejoin the routine at draw29 for the next edge .draw16 \ If we get here then we are drawing either a left or \ right edge, and Y is 1 or 2 respectively BNE draw18 \ We did a CPY #1 before jumping here, so this jumps to \ draw18 if Y <> 1, i.e. Y = 2, so we jump to draw18 if \ we are drawing a right edge \ If we get here then we are drawing a left edge in \ pixel X within the pixel byte, so we now calculate the \ pixel byte for the left edge LDA pixelsToLeft,X \ Set A to the X-th pixel mask from pixelsToLeft, which \ is a pixel byte with all the pixels set to the left of \ the X-th pixel - in other words, with all the pixels \ outside of the object (to the left of the left edge) \ set STA L \ Store this pixel mask in L, so L contains a pixel mask \ containing the pixels to the left of the left edge EOR #&FF \ Invert A so it contains a pixel byte with the X-th \ pixel set, plus all the pixels to the right - in other \ words, with the edge and the inside of the object set AND rightOfEdge \ Set the object pixels to rightOfEdge, which contains \ the fill colour for the object (as at this point we \ won't be sharing a pixel byte with the previous edge, \ as there is no previous edge) AND yLookupLo+8,X \ Apply the X-th pixel mask from yLookupLo+8, so this \ clears the X-th pixel in the pixel byte ORA edgePixel \ We set up edgePixel above to contain a single pixel \ for the edge in position X, set to the edge colour, \ so this sets the X-th pixel in A to the edge colour, \ so the pixel byte in A now contains the edge itself .draw17 \ If we get here then we are either drawing a left edge, \ or this is a four-edge object part and we are sharing \ this pixel byte with the previous edge LDX blockNumber \ If blockNumber <> nextBlockNumber, then the next edge CPX nextBlockNumber \ isn't in the same dash data block (i.e. in the same BNE draw19 \ column) as this one, so jump to draw19 to draw this \ edge \ If we get here then: \ \ * The next edge is in the same pixel byte as the \ edge we are currently drawing, so we don't draw \ this byte yet, but instead return from the routine \ so the next call to DrawObjectEdge can pick up the \ baton and insert the next edge into the byte we \ just created \ \ * The C flag is set, as the above comparison was \ blockNumber = nextBlockNumber STA rightOfEdge \ Set rightOfEdge to the pixel byte in A, ready for \ the next call to DrawObjectEdge to pick it up LDA L \ Set edgePixelMask to the pixel mask in L, which STA edgePixelMask \ contains the pixels to the left of the left edge, to \ pass to the next call to DrawObjectEdge ROR prevEdgeInByte \ Set bit 7 of prevEdgeInByte, so it is non-zero for the \ next call to DrawObjectEdge, to indicate that the next \ edge needs to be inserted into the pixel byte that we \ just built, alongside the current edge RTS \ Return from the subroutine .draw18 \ If we get here then we are drawing a right edge in \ pixel X within the pixel byte, so we now calculate the \ pixel byte for the right edge \ \ This routine draws the right edge in the pixel byte, \ making sure we keep any previous edge that's already \ drawn in the same pixel byte LDA edgePixelMask \ Set A to the edge pixel mask of the previous edge, \ which will: \ \ * Be empty if the previous edge was already drawn \ and we do not need to share this pixel byte with \ the previous edge \ \ * Contain set pixels for the previous edge if both \ this edge and the previous edge need to share the \ same pixel byte ORA pixelsToRight,X \ The pixelsToRight table contains pixel bytes with all \ the pixels set to the right of the X-th pixel, so this \ sets all the pixels to the right of the X-th pixel \ A now contains a pixel byte with pixels set to the \ right of this edge, and if the previous edge is in the \ same byte, pixels are also set to the left of that \ edge STA L \ Store this pixel mask in L so we can use it in part 4, \ so L contains a pixel mask containing the pixels to \ the right of the right edge (and the left of the \ previous edge, if it's in the same byte) EOR #&FF \ Invert A so it contains the pixels of this edge, plus \ the previous edge if this was drawn in the same byte AND leftOfEdge \ Set these pixels to leftOfEdge, which is either the \ fill colour of the object (if there is just this edge \ in the pixel byte), or it's the pixel byte of the \ previous edge (if we have both edges in the same byte) \ \ In either case, this fills the pixel byte with the \ correct contents AND yLookupLo+8,X \ Apply the X-th pixel mask from yLookupLo+8, so this \ clears the X-th pixel in the pixel byte ORA edgePixel \ We set up edgePixel above to contain a single pixel \ for the edge in position X, set to the edge colour, \ so this sets the X-th pixel in A to the edge colour, \ so the pixel byte in A now contains the edge itself \ So we now have a pixel byte in A that contains this \ edge in the correct colour, and if this pixel byte \ also contains the previous edge, that's in there too \ ****************************************************************************** \ \ Name: DrawObjectEdge (Part 4 of 5) \ Type: Subroutine \ Category: Drawing objects \ Summary: Draw the edge into the screen buffer, merging with any content \ already in the buffer \ Deep dive: Creating objects from edges \ \ ****************************************************************************** .draw19 \ By this point, we have the following: \ \ * A contains the pixel byte we need to use to draw \ this edge (incorporating the previous edge if it's \ close enough to be in the same pixel byte) \ \ * L contains a pixel mask containing set pixels for \ those pixels outside of the object and its fill, \ so to the left of the left edge, or to the right \ of the right edge, or to the left and right \ of pixel byte that contains two edges \ \ * (Q P) contains the address of the dash data block \ containing the edge we want to draw \ \ * blockOffset contains the dash data block offset \ for the bottom track line of the edge we want to \ draw \ \ * topTrackLine contains the number of the track line \ at the top of the edge we want to draw \ \ So now we actually draw the edge STA I \ Set I to the pixel byte for the edge we want to draw IF _ACORNSOFT OR _4TRACKS BNE draw20 \ If the pixel byte is non-zero, jump to draw20 to skip \ the following instruction LDA #&55 \ Set A = &55 to use as the value of WW below (&55 in \ the screen buffer represents colour 0, or black) .draw20 STA WW \ Set WW to the pixel byte (or &55 if the pixel byte is \ zero) LDA #0 \ Set edgePixelMask = 0, so the next call to STA edgePixelMask \ DrawObjectEdge ignores any edges from this call (as \ we are about to draw them on-screen) LDY blockOffset \ Set W to the current screen buffer byte at the bottom LDA (P),Y \ of the edge that we want to draw STA W LDA #&AA \ Replace the byte at the bottom of the edge with &AA, STA (P),Y \ to use as a marker LDY topTrackLine \ Set Y = topTrackLine, which is the number of the \ track line at the top of the object, which is the \ same as the offset into the dash data block for the \ top edge JMP draw24 \ Jump into the following loop at the entry point draw24 \ to draw the edge from the top byte to the bottom byte \ \ We jump into the loop with the following set: \ \ * Y contains the offset of the top of the edge we \ want to draw, which we now use as a loop counter \ to work our way from the top of the edge to the \ bottom (i.e. Y gets decremented as we draw each \ pixel byte) \ \ * The bottom track line of the edge contains the \ marker byte &AA \ \ * W contains the original byte that was in the \ marker's location .draw21 \ We loop back here if we fetch the next byte down from \ the screen buffer into A and find that it is non-zero CMP #&55 \ If the current byte is &55, then this represents black BNE draw22 \ (colour 0), so set A = 0 so it's the correct pixel LDA #0 \ byte for the current buffer contents .draw22 AND L \ The bit mask in L contains set pixels for those \ outside of the edge and its fill, and clear pixels for \ the edge and object fill, so this clears the pixels \ where we want to draw the edge and object fill ORA I \ Replace those cleared bits with the edge and fill that \ we want to draw BNE draw23 \ If the result is non-zero, then jump to draw23 to skip \ the following instruction LDA #&55 \ The result is zero, i.e. colour 0 (black), so set \ A = &55, which is the value we use to represent \ colour 0 (black) in the screen buffer .draw23 STA (P),Y \ Draw the resulting pixel byte into the screen buffer \ by writing it to the Y-th byte of the relevant dash \ data block DEY \ Decrement the track line counter in Y to move down to \ the next pixel line on-screen .draw24 \ This is the entry point for the loop, which is between \ draw21 and the loop's end logic at draw28 LDA (P),Y \ If the current byte in the screen buffer is non-zero, BNE draw28 \ then it is not empty, so jump to draw28 to merge the \ pixel byte with the non-empty byte that's already in \ the screen buffer .draw25 JSR GetColour \ The current byte in the screen buffer is zero, which \ means it should inherit the colour of the byte to the \ left, so call GetColour to work out what this byte's \ colour would be on-screen, and put it into A AND L \ The bit mask in L contains set pixels for those \ outside of the edge and its fill, and clear pixels for \ the edge and object fill, so this clears the pixels \ where we want to draw the edge and object fill ORA I \ Replace those cleared bits with the edge and fill that \ we want to draw BNE draw26 \ If the result is non-zero, then jump to draw23 to skip \ the following instruction LDA #&55 \ The result is zero, i.e. colour 0 (black), so set \ A = &55, which is the value we use to represent \ colour 0 (black) in the screen buffer .draw26 STA (P),Y \ Draw the resulting pixel byte into the screen buffer \ by writing it to the Y-th byte of the relevant dash \ data block DEY \ Decrement the track line counter in Y to move down to \ the next pixel line on-screen .draw27 LDX &3000,Y \ Set X to the Y-th byte in the screen buffer, which is \ the next byte down the screen after the one we just \ draw \ \ Gets modified at the start of part 2 as follows: \ \ LDX &3000,Y -> LDX #(Q P),Y \ \ In other words, we have modified the instruction to \ load the Y-th byte from the dash data block address \ for the edge we are drawing, i.e. load the Y-th byte \ of dash data block blockNumber BEQ draw26 \ If the next byte down the screen is zero, then loop \ back to draw26 to draw the edge in this byte as well, \ so we keep drawing the edge downwards while the \ existing contents of the screen buffer are empty TXA \ The next byte down the screen is non-zero, so copy the \ value into A .draw28 CMP #&AA \ If this is not the marker for the bottom of the edge, BNE draw21 \ loop back to draw21 to draw the next pixel byte over \ the top of this non-empty byte in the screen buffer \ We just reached a value that matches the marker at the \ bottom of the edge, which is either our marker or a \ valid entry in the screen buffer that happens to have \ this value LDA #0 \ Overwrite the &AA value with 0, though this appears to STA (P),Y \ have no effect, as if we loop back to draw25 in the \ following conditional, this value will be overwritten, \ and if we fall through to the LDA W below, it will \ be overwritten there CPY blockOffset \ If Y <> blockOffset then this can't be our marker, as BNE draw25 \ the marker is on line blockOffset, so jump back to \ draw25 to merge this byte with the correct background \ colour \ If we get here then we have reached our marker at the \ bottom of the edge LDA W \ Restore the entry in the dash data block that we STA (P),Y \ overwrote with the marker, whose original contents we \ stored in W LDX J \ If the edge type in J = 1, then we have just drawn the CPX #1 \ left edge, so jump to draw31 to return from the BEQ draw31 \ subroutine as we are done drawing \ If we get here then we have just drawn an extra or \ right edge, so we need to fill the next column to the \ right of the edge we just drew (i.e. in the next block \ along), so that the background colour is restored \ after the object INC blockNumber \ Fill the column to the right of the edge we just drew, JSR FillAfterObject \ so the correct background colour is shown to the right DEC blockNumber \ of the object part ELIF _SUPERIOR OR _REVSPLUS LDA #0 \ Set edgePixelMask = 0, so the next call to STA edgePixelMask \ DrawObjectEdge ignores any edges from this call (as \ we are about to draw them on-screen) LDY topTrackLine \ Set Y = topTrackLine, which is the number of the \ track line at the top of the object, which is the \ same as the offset into the dash data block for the \ top edge JMP sraw6 \ Jump into the following loop at the entry point sraw6 \ to draw the edge from the top byte to the bottom byte \ \ We jump into the loop with the following set: \ \ * Y contains the offset of the top of the edge we \ want to draw, which we now use as a loop counter \ to work our way from the top of the edge to the \ bottom (i.e. Y gets decremented as we draw each \ pixel byte) \ \ * The bottom track line of the edge contains the \ marker byte &AA \ \ * W contains the original byte that was in the \ marker's location .sraw1 LDA (P),Y \ If the current byte in the screen buffer is zero, then BEQ sraw2 \ jump to sraw2 to work out what colour it would be \ on-screen CMP #&55 \ If the current byte is not &55, then the current byte BNE sraw3 \ in the screen buffer contains something, so jump to \ sraw3 with the contents in A LDA I \ The current byte in the screen buffer is &55, or \ black, so set A to the pixel byte we want to draw BNE sraw5 \ If we want to draw something that isn't black (i.e. \ which is non-zero), jump to sraw5 to store it in the \ screen buffer BEQ sraw4 \ If we get here then we want to draw a black pixel \ byte, so jump to sraw4 to store &55 in the screen \ buffer (which is the screen buffer value for black) .sraw2 JSR GetColourSup \ The current byte in the screen buffer is zero, which \ means it should inherit the colour of the byte to the \ left, so call GetColourSup to work out what this \ byte's colour would be on-screen, and put it into A .sraw3 \ At this point A contains the byte in the screen \ buffer, converted into the byte it represents when \ the buffer is copied to the screen AND L \ The bit mask in L contains set pixels for those \ outside of the edge and its fill, and clear pixels for \ the edge and object fill, so this clears the pixels \ where we want to draw the edge and object fill ORA I \ Replace those cleared bits with the edge and fill that \ we want to draw BNE sraw5 \ If the result is non-zero, then jump to sraw5 to skip \ the following instruction .sraw4 LDA #&55 \ Set A = &55, which is the value we use to represent \ colour 0 (black) in the screen buffer .sraw5 STA (P),Y \ Draw the resulting pixel byte into the screen buffer \ by writing it to the Y-th byte of the relevant dash \ data block DEY \ Decrement the track line counter in Y to move down to \ the next pixel line on-screen .sraw6 \ This is the entry point for the loop, which is between \ sraw1 and the loop's end logic in the next instruction CPY blockOffset \ If Y <> blockOffset then this can't be our marker, as BNE sraw1 \ the marker is on line blockOffset, so jump back to \ sraw1 to merge this byte with the correct background \ colour LDX J \ If the edge type in J = 1, then we have just drawn the CPX #1 \ left edge, so jump to draw31 to return from the BEQ draw31 \ subroutine as we are done drawing \ If we get here then we have just drawn an extra or \ right edge, so we need to fill the next column to the \ right of the edge we just drew (i.e. in the next block \ along), so that the background colour is restored \ after the object INC blockNumber \ Fill the column to the right of the edge we just drew, JSR FillAfterObjectSup \ so the correct background colour is shown to the right DEC blockNumber \ of the object part ENDIF \ ****************************************************************************** \ \ Name: DrawObjectEdge (Part 5 of 5) \ Type: Subroutine \ Category: Drawing objects \ Summary: Fill the object if required and loop back for the next edge \ Deep dive: Creating objects from edges \ \ ------------------------------------------------------------------------------ \ \ Other entry points: \ \ draw29 Fill the inside of the object part from the previous \ block to the current one \ \ ****************************************************************************** .draw29 \ We have finished drawing the edge, so now we need to \ fill the inside of the object part, from the previous \ block to the current one LDA prevBlockNumber \ If prevBlockNumber < 40, then it is a valid block CMP #40 \ number in the range 0 to 39, so jump to draw30 to skip BCC draw30 \ the following instruction and fill the object from \ this block onwards \ If we get here then prevBlockNumber is off-screen, \ which means it must be off the left edge of the screen \ (as we are working from left to right), so we now need \ to fill from the left edge of the screen to the edge \ we just drew LDA #&FF \ Set prevBlockNumber = -1, so the following subtraction STA prevBlockNumber \ sets: \ \ A = blockNumber - prevBlockNumber - (1 - C) \ = blockNumber - -1 - 1 \ = blockNumber \ \ so the following fills this many blocks, which means \ it fills all the blocks from the left edge of the \ screen to the edge we just drew .draw30 LDA blockNumber \ Set A = blockNumber - prevBlockNumber - (1 - C) CLC \ = blockNumber - prevBlockNumber - 1 SBC prevBlockNumber BEQ draw31 \ If A <= 0, then: BMI draw31 \ \ blockNumber - prevBlockNumber - 1 <= 0 \ \ blockNumber - prevBlockNumber <= 1 \ \ so the current block and the previous block are either \ the same block or neighbours, in which case there is \ no gap to fill between the edges, so jump to draw31 to \ return from the subroutine as we are done drawing \ If we get here then A > 0, so from the above: \ \ blockNumber - prevBlockNumber > 1 \ \ so there is at least one full block between the \ current block and the previous block \ \ We therefore need to fill this gap with the relevant \ fill colour, with the number of blocks between the \ two edges given in A (without including the edges \ themselves) TAX \ Set X = A, so X contains the number of blocks we need \ to fill to the left of the edge we just drew JSR FillInsideObject \ Fill the inside of the object (i.e. all the blocks \ between the previous edge and the edge we just drew) .draw31 RTS \ Return from the subroutine .draw32 \ We jump here if the block number in blockNumber \ is >= 40, which means we the edge we are trying to \ draw is off the right of the screen, so now we need \ to work out whether we need to fill the object up to \ the edge of the screen LDY J \ If the edge type in J = 1, then we are drawing the CPY #1 \ left edge, so jump to draw31 to return from the BEQ draw31 \ subroutine as the whole object part is off-screen LDA prevBlockNumber \ If prevBlockNumber >= 40, then the dash data block CMP #40 \ number from the previous call to DrawObjectEdge is BCS draw31 \ also past the right edge of the screen, so jump to \ draw31 to return from the subroutine as the whole \ part between this edge and the previous edge is \ off-screen \ Otherwise we are drawing a right edge or an extra edge \ and the previous call to DrawObjectEdge was on-screen, \ so we need to fill between the previous edge and the \ right edge of the screen LDA #40 \ Set blockNumber = 40 to represent the block beyond the STA blockNumber \ right edge of the screen in the calculation at draw30, \ which works out whether to call FillInsideObject to \ fill from the previous edge to this block number BNE draw30 \ Jump to draw30 (this BNE is effectively a JMP as A is \ never zero) \ ****************************************************************************** \ \ Name: GetTyreDashEdge \ Type: Subroutine \ Category: Dashboard \ Summary: Copy the pixel bytes along the tyre and dashboard edges so they \ can be feathered \ \ ------------------------------------------------------------------------------ \ \ Modify the FillAfterObject routine before calling it. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X Sets the destination address for the copy \ \ Y Alters the routine flow to either fill or copy \ \ A The value to use when copying black pixel bytes \ \ ****************************************************************************** IF _ACORNSOFT OR _4TRACKS .GetTyreDashEdge STX edge7+1 \ Modify the following instruction at edge7: \ \ STA (P),Y -> STA (R),Y when X = LO(R) \ \ STA (P),Y -> STA (P),Y when X = LO(P) STY edge11+1 \ Modify the following instruction at edge11: \ \ BNE edge3 -> BNE edge1 when Y = &DF \ \ BNE edge3 -> BNE edge3 when Y = &E7 STA edge6+1 \ Modify the following instruction at edge6: \ \ LDA #&55 -> LDA #0 when A = 0 \ \ LDA #&55 -> LDA #&55 when A = &55 \ Fall through into FillAfterObject to copy the edge \ data to the location specified in (S R) ENDIF \ ****************************************************************************** \ \ Name: FillAfterObject \ Type: Subroutine \ Category: Drawing objects \ Summary: Fill the block to the right of an object \ Deep dive: Creating objects from edges \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ blockNumber The dash data block number to draw in (i.e. the block \ to the right of the object) \ \ blockOffset The dash data offset for the bottom of the edge to draw \ \ topTrackLine Top track line number, i.e. the number of the start byte \ in the dash data block \ \ ****************************************************************************** IF _ACORNSOFT OR _4TRACKS .FillAfterObject LDA blockNumber \ Set A to the dash data block number in blockNumber CMP #40 \ If A >= 40 then this is not a valid dash data block BCS edge12 \ number, so jump to edge12 to return from the \ subroutine \ We now calculate the start address of dash data block \ A, which will be at dashData + &80 * A (because the \ dash data blocks occur every &80 bytes from dashData) \ \ We do this using the following simplification: \ \ dashData + &80 * A \ = dashData + 256 / 2 * A \ = HI(dashData) << 8 + LO(dashData) + A << 7 \ \ LO(dashData) happens to be zero (as dashData = &3000), \ so we can keep going: \ \ = HI(dashData) << 8 + A << 7 \ = (HI(dashData) << 1 + A) << 7 \ = ((HI(dashData) << 1 + A) << 8) >> 1 \ \ In other words, if we build a 16-bit number with the \ high byte set to HI(dashData) << 1 + A, and then shift \ the whole thing right by one place, we have our result \ \ We do this below, storing the 16-bit number in (Q P) CLC \ Set A = A + HI(dashData) << 1 ADC #HI(dashData)<<1 \ \ so our 16-bit number is (A 0), and we want to shift \ right by one place LSR A \ Shift (A 0) right by 1, shifting bit 0 of A into the \ C flag STA Q \ Set Q = A, to store the high byte of the result in Q STA edge9+2 \ Modify the high byte of the address in the instruction \ at edge9 to Q LDA #0 \ Shift the C flag into bit 7 of A, so A now contains ROR A \ the low byte of our result STA P \ Set P = A, to store the low byte of the result in P, \ giving the result we wanted in (Q P) STA edge9+1 \ Modify the low byte of the address in the instruction \ at edge9 to P, so if we have the following: \ \ LDX &3000,Y -> LDX #(Q P),Y \ \ This is pseudo-code, but it means we have modified the \ instruction to load the Y-th byte from the dash data \ block address we just calculated, i.e. load the Y-th \ byte of dash data block A (i.e. dash data block \ blockNumber) LDY blockOffset \ Set Y to the dash data offset for the edge to draw LDA (P),Y \ Set W to the byte in the dash data offset from this STA W \ block, which is the byte before the actual dash data LDA #&AA \ Store &AA in this byte, so it can act as a marker for STA (P),Y \ when we work our way through the data below LDY topTrackLine \ Set Y to the number of the top track line, so we work \ down from this byte within the data block, moving down \ in memory until we reach the marker \ \ So we are working down the screen, going backwards in \ memory from byte topTrackLine to the marker that we \ just placed at the start of the dash data JMP edge4 \ Jump into the following loop at the entry point edge4 \ to draw the fill from the top byte to the bottom byte .edge1 \ This part of the loop, between edge1 and edge5, is \ only used by the GetTyreDashEdge routine, which \ modifies the loop to copy pixels instead of filling \ them CMP #&55 \ If the current byte is &55, then this represents black BNE edge2 \ (colour 0), so set A = 0 so it's the correct pixel LDA #0 \ byte for the current buffer contents .edge2 STA (R),Y \ Store the pixel byte we fetched from the screen buffer \ in the Y-th byte of (S R), which copies the byte from \ the screen buffer into the address set up in the \ CopyTyreDashEdges routine (i.e. this copies the edges \ into tyreRightEdge or dashRightEdge) .edge3 DEY \ Decrement the byte counter to move down the screen \ within the dash data block .edge4 LDA (P),Y \ Fetch the Y-th byte from the dash data block BNE edge10 \ If the current byte in the screen buffer is non-zero, \ then it is not empty, so jump to edge10 to move on to \ the next byte, as we only need to fill empty bytes .edge5 JSR GetColour \ The current byte in the screen buffer is zero, which \ means it should inherit the colour of the byte to the \ left, so call GetColour to work out what this byte's \ colour would be on-screen, and put it into A BNE edge7 \ If the colour byte is non-zero, skip the following \ instruction .edge6 LDA #&55 \ Set A = &55, which is the value we use to represent \ colour 0 (black) in the screen buffer \ \ Gets modified by the GetTyreDashEdge routine: \ \ * LDA #0 when GetTyreDashEdge is called with \ A = 0 \ \ * LDA #&55 when GetTyreDashEdge is called with \ A = &55 .edge7 STA (P),Y \ Draw the resulting pixel byte into the screen buffer \ by writing it to the Y-th byte of the relevant dash \ data block \ \ Gets modified by the GetTyreDashEdge routine: \ \ * STA (R),Y when GetTyreDashEdge is called with \ X = LO(R) \ \ * STA (P),Y when GetTyreDashEdge is called with \ X = LO(P) .edge8 DEY \ Decrement the byte counter to move down the screen \ within the dash data block .edge9 LDX &3000,Y \ Set X to the Y-th byte in the screen buffer, which is \ the next byte down the screen after the one we just \ draw \ \ Gets modified at the start of part 2 as follows: \ \ LDX &3000,Y -> LDX #(Q P),Y \ \ In other words, we have modified the instruction to \ load the Y-th byte from the dash data block address \ for the edge we are drawing, i.e. load the Y-th byte \ of dash data block blockNumber BEQ edge7 \ If the next byte down the screen is zero, then loop \ back to edge7 to draw the edge in this byte as well, \ so we keep drawing the edge downwards while the \ existing contents of the screen buffer are empty TXA \ The next byte down the screen is non-zero, so copy the \ value into A .edge10 CMP #&AA \ Check to see if the next byte is the marker for the \ bottom of the edge .edge11 BNE edge3 \ If this is not the marker for the bottom of the edge, \ loop back to edge3 to draw the next pixel byte over \ the top of this non-empty byte in the screen buffer \ \ Gets modified by the GetTyreDashEdge routine: \ \ * BNE edge1 when GetTyreDashEdge is called with \ Y = &DF \ \ * BNE edge3 when GetTyreDashEdge is called with \ Y = &E7 LDA #0 \ Overwrite the &AA value with 0 to remove the marker STA (P),Y CPY blockOffset \ If Y <> blockOffset then this can't be our marker, as BNE edge5 \ the marker is on line blockOffset, so jump back to \ edge5 to merge this byte with the correct background \ colour \ If we get here then we have reached our marker at the \ bottom of the edge LDA W \ Restore the entry in the dash data block that we STA (P),Y \ overwrote with the marker, whose original contents we \ stored in W .edge12 RTS \ Return from the subroutine ENDIF \ ****************************************************************************** \ \ Name: GetTyreDashEdgeSup \ Type: Subroutine \ Category: Dashboard \ Summary: Copy the pixel bytes along the tyre and dashboard edges so they \ can be feathered \ \ ------------------------------------------------------------------------------ \ \ Modify the FillAfterObject routine before calling it. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X Sets the destination address for the copy \ \ Y Alters the routine flow to either fill or copy \ \ A The value to use when copying black pixel bytes \ \ ****************************************************************************** IF _SUPERIOR OR _REVSPLUS .GetTyreDashEdgeSup STX sedg7+1 \ Modify the following instruction at sedg7: \ \ STA (P),Y -> STA (R),Y when X = LO(R) \ \ STA (P),Y -> STA (P),Y when X = LO(P) STY sedg5+1 \ Modify the following instruction at sedg5: \ \ BNE sedg8 -> BNE sedg1 when Y = &EF \ \ BNE sedg8 -> BNE sedg8 when Y = &09 STA sedg6+1 \ Modify the following instruction at sedg6: \ \ LDA #&55 -> LDA #0 when A = 0 \ \ LDA #&55 -> LDA #&55 when A = &55 \ Fall through into FillAfterObjectSup to copy the edge \ data to the location specified in (S R) ENDIF \ ****************************************************************************** \ \ Name: FillAfterObjectSup \ Type: Subroutine \ Category: Drawing objects \ Summary: Fill the block to the right of an object \ Deep dive: Creating objects from edges \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ blockNumber The dash data block number to draw in (i.e. the block \ to the right of the object) \ \ blockOffset The dash data offset for the bottom of the edge to draw \ \ topTrackLine Top track line number, i.e. the number of the start byte \ in the dash data block \ \ ****************************************************************************** IF _SUPERIOR OR _REVSPLUS .FillAfterObjectSup LDA blockNumber \ Set A to the dash data block number in blockNumber CMP #40 \ If A >= 40 then this is not a valid dash data block BCS sedg10 \ number, so jump to sedg10 to return from the \ subroutine \ We now calculate the start address of dash data block \ A, which will be at dashData + &80 * A (because the \ dash data blocks occur every &80 bytes from dashData) \ \ We do this using the following simplification: \ \ dashData + &80 * A \ = dashData + 256 / 2 * A \ = HI(dashData) << 8 + LO(dashData) + A << 7 \ \ LO(dashData) happens to be zero (as dashData = &3000), \ so we can keep going: \ \ = HI(dashData) << 8 + A << 7 \ = (HI(dashData) << 1 + A) << 7 \ = ((HI(dashData) << 1 + A) << 8) >> 1 \ \ In other words, if we build a 16-bit number with the \ high byte set to HI(dashData) << 1 + A, and then shift \ the whole thing right by one place, we have our result \ \ We do this below, storing the 16-bit number in (Q P) CLC \ Set A = A + HI(dashData) << 1 ADC #HI(dashData)<<1 \ \ so our 16-bit number is (A 0), and we want to shift \ right by one place LSR A \ Shift (A 0) right by 1, shifting bit 0 of A into the \ C flag STA Q \ Set Q = A, to store the high byte of the result in Q LDA #0 \ Shift the C flag into bit 7 of A, so A now contains ROR A \ the low byte of our result STA P \ Set P = A, to store the low byte of the result in P, \ giving the result we wanted in (Q P) LDY topTrackLine \ Set Y to the number of the top track line, so we work \ down from this byte within the data block, moving down \ in memory until we reach the marker \ \ So we are working down the screen, going backwards in \ memory from byte topTrackLine to the marker that we \ just placed at the start of the dash data JMP sedg9 \ Jump into the following loop at the entry point sedg9 \ to draw the fill from the top byte to the bottom byte .sedg1 \ This part of the loop, between segd1 and segd5, is \ only used by the GetTyreDashEdgeSup routine, which \ modifies the loop to copy pixels instead of filling \ them CMP #&55 \ If the current byte is &55, then this represents black BNE sedg2 \ (colour 0), so set A = 0 so it's the correct pixel LDA #0 \ byte for the current buffer contents .sedg2 STA (R),Y \ Store the pixel byte we fetched from the screen buffer \ in the Y-th byte of (S R), which copies the byte from \ the screen buffer into the address set up in the \ CopyTyreDashEdges routine (i.e. this copies the edges \ into tyreRightEdge or dashRightEdge) .sedg3 DEY \ Decrement the track line counter in Y to move down to \ the next pixel line on-screen CPY blockOffset \ If Y = blockOffset then we have reached the bottom of BEQ sedg10 \ the object, so jump back to sedg10 to return from the \ subroutine .sedg4 LDA (P),Y \ Fetch the current byte from the screen buffer .sedg5 BNE sedg8 \ If the current byte in the screen buffer is non-zero, \ then it is not empty, so jump to sedg8 to move on to \ the next byte, as we only need to fill empty bytes \ \ Gets modified by the GetTyreDashEdge routine: \ \ * BNE sedg1 when GetTyreDashEdge is called with \ Y = &EF \ \ * BNE sedg8 when GetTyreDashEdge is called with \ Y = &09 JSR GetColourSup \ The current byte in the screen buffer is zero, which \ means it should inherit the colour of the byte to the \ left, so call GetColourSup to work out what this \ byte's colour would be on-screen, and put it into A BNE sedg7 \ If the result is non-zero, then jump to sedg7 to skip \ the following instruction .sedg6 LDA #&55 \ Set A = &55, which is the value we use to represent \ colour 0 (black) in the screen buffer \ \ Gets modified by the GetTyreDashEdge routine: \ \ * LDA #0 when GetTyreDashEdge is called with \ A = 0 \ \ * LDA #&55 when GetTyreDashEdge is called with \ A = &55 .sedg7 STA (P),Y \ Draw the resulting pixel byte into the screen buffer \ by writing it to the Y-th byte of the relevant dash \ data block \ \ Gets modified by the GetTyreDashEdge routine: \ \ * STA (R),Y when GetTyreDashEdge is called with \ X = LO(R) \ \ * STA (P),Y when GetTyreDashEdge is called with \ X = LO(P) .sedg8 DEY \ Decrement the track line counter in Y to move down to \ the next pixel line on-screen .sedg9 \ This is the entry point for the loop, which is between \ sedg4 and the loop's end logic in the next instruction CPY blockOffset \ If Y <> blockOffset then we haven't reached the bottom BNE sedg4 \ of the object yet, so jump back to sedg4 to fill the \ next byte down .sedg10 RTS \ Return from the subroutine ENDIF \ ****************************************************************************** \ \ Name: DrawEdge \ Type: Subroutine \ Category: Drawing objects \ Summary: Draw an edge, overwriting whatever is already on-screen \ \ ------------------------------------------------------------------------------ \ \ This routine rejoins the DrawObjectEdge routine to move on to the next edge in \ the current object part. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ (Q P) The address of the dash data block to draw in \ \ Y The top track line \ \ blockOffset The bottom track line \ \ A The pixel byte for the edge \ \ ****************************************************************************** .dred1 STA (P),Y \ Draw the pixel byte into the screen buffer by writing \ it to the Y-th byte of the relevant dash data block DEY \ Decrement the track line counter in Y to move down to \ the next pixel line on-screen .DrawEdge \ This is the entry point for the routine CPY blockOffset \ If Y <> blockOffset then loop back to draw the next BNE dred1 \ byte, as we haven't reached the bottom track line JMP draw29 \ Jump to draw29 to fill the inside of the object part \ from the previous block to the current one \ ****************************************************************************** \ \ Name: GetTyreDashEdges \ Type: Subroutine \ Category: Dashboard \ Summary: Fetch the pixel bytes from along the edge of the dashboard or tyre \ and fill the block to the right of the edge appropriately \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The number of the leftmost dash data block to copy \ \ A The number of the dash data block after the last block \ to copy (so the last block to draw is A - 1) \ \ Y Start at this byte in the dash data, so we work down the \ screen from track line Y \ \ (S R) The address of the table into which we copy the pixel \ bytes from the specified edge \ \ ****************************************************************************** .GetTyreDashEdges STA blockCounter \ Set a loop counter in blockCounter, so the following, \ X loops from blockNumber to blockCounter - 1 \ (i.e. A - 1) .gedg1 STX blockNumber \ Store the loop counter in blockNumber STY topTrackLine \ Set topTrackLine to the offset of the start byte LDA dashDataOffset,X \ Set blockOffset = the dash data offset for block X STA blockOffset LDX #LO(R) \ Set X so the call to GetTyreDashEdge modifies the \ FillAfterObject routine to draw to (S R) instead of \ (Q P) IF _ACORNSOFT OR _4TRACKS LDY #&DF \ Set Y = &DF so the call to GetTyreDashEdge modifies \ the FillAfterObject routine at edge11 to BNE edge1, \ so the routine copies into (S R) instead of filling \ the screen buffer LDA #0 \ Set A = 0, so the call to GetTyreDashEdge modifies the \ FillAfterObject routine to store 0 as the value for \ colour 0 (instead of the &55 that the screen buffer \ uses to represent black) JSR GetTyreDashEdge \ Modify the FillAfterObject routine and run it to copy \ the edge bytes into the table at (S R) LDX #LO(P) \ Set X so the call to GetTyreDashEdge modifies the \ FillAfterObject routine back to drawing to (Q P) LDY #&E7 \ Set Y = &E7 so the call to GetTyreDashEdge modifies \ the FillAfterObject routine at edge11 back to BNE \ edge3 LDA #&55 \ Set A = &55, so the call to GetTyreDashEdge modifies \ the FillAfterObject routine back to storing &55 as the \ value for colour 0 INC blockNumber \ Increment the block number JSR GetTyreDashEdge \ Modify the FillAfterObject routine back to its default \ code and run it, which fills the block to the right of \ the dashboard or tyre edge with the appropriate \ content ELIF _SUPERIOR OR _REVSPLUS LDY #&EF \ Set Y = &DF so the call to GetTyreDashEdgeSup modifies \ the FillAfterObjectSup routine at sedg5 to BNE sedg1, \ so the routine copies into (S R) instead of filling \ the screen buffer LDA #0 \ Set A = 0, so the call to GetTyreDashEdgeSup modifies \ the FillObject routine to store 0 as the value for \ colour 0 (instead of the &55 that the screen buffer \ uses to represent black) JSR GetTyreDashEdgeSup \ Modify the FillAfterObjectSup routine and run it to \ copy the edge bytes into the table at (S R) LDX #LO(P) \ Set X so the call to GetTyreDashEdgeSup modifies the \ FillAfterObjectSup routine at back to drawing to (Q P) LDY #&09 \ Set Y = &09 so the call to GetTyreDashEdgeSup modifies \ the FillAfterObjectSup routine at sedg5 back to BNE \ sedg8 LDA #&55 \ Set A = &55, so the call to GetTyreDashEdgeSup \ modifies the FillAfterObjectSup routine back to \ storing &55 as the value for colour 0 INC blockNumber \ Increment the block number JSR GetTyreDashEdgeSup \ Modify the FillAfterObjectSup routine back to its \ default code and run it, which fills the block to the \ right of the dashboard or tyre edge with the \ appropriate content ENDIF LDX blockNumber \ Fetch the loop counter from blockNumber into X CPX blockCounter \ If X <> blockCounter, loop back until we have copied BNE gedg1 \ from block blockNumber to block blockCounter - 1 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: CopyTyreDashEdges \ Type: Subroutine \ Category: Dashboard \ Summary: Fetch the pixel bytes from the right edge of the left tyre and the \ right edge of the dashboard, and fill to the right of the edge \ \ ------------------------------------------------------------------------------ \ \ This routine populates the tyreRightEdge and dashRightEdge tables with the \ pixel bytes along the right edge of the left tyre and the right edge of the \ dashboard respectively. It also fills the block to the right of the edge with \ the appropriate content, so the feathered edges don't fill to the right. \ \ ****************************************************************************** .CopyTyreDashEdges LDA #HI(tyreRightEdge) \ Set (S R) = tyreRightEdge STA S \ LDA #LO(tyreRightEdge) \ so the call to GetTyreDashEdges copies the pixel data STA R \ from the tyre edge into the tyreRightEdge table LDY #27 \ Start at byte 27 in the dash data, so we work down the \ screen from track line 27 LDX #3 \ Loop through dash data blocks 3 to 5 LDA #6 JSR GetTyreDashEdges \ Fetch the pixel bytes from along the right edge of the \ left tyre and fill the block to the right of the edge \ with the appropriate content LDA #HI(dashRightEdge) \ Set (S R) = dashRightEdge STA S \ LDA #LO(dashRightEdge) \ so the call to GetTyreDashEdges copies the pixel data STA R \ from the dashboard edge into the dashRightEdge table LDY #43 \ Start at byte 43 in the dash data, so we work down the \ screen from track line 43 LDX #26 \ Loop through dash data blocks 26 to 33 LDA #34 JSR GetTyreDashEdges \ Fetch the pixel bytes from along the right edge of the \ dashboard and fill the block to the right of the edge \ with the appropriate content RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: FillInsideObject \ Type: Subroutine \ Category: Drawing objects \ Summary: Fill the object part from the previous edge to the current edge \ Deep dive: Creating objects from edges \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The number of bash data blocks inside the object that we \ need to fill \ \ blockNumber The block number of the edge to fill up to (i.e. we fill \ to the left of this block number) \ \ leftOfEdge The pixel byte to fill the object with \ \ topTrackLine Top track line of the object (higher value, 0 to 79) \ \ bottomTrackLine Bottom track line of the object (lower value, 0 to 79) \ \ ****************************************************************************** .FillInsideObject LDA leftOfEdge \ Set A to the byte we want to fill the object with BNE fill1 \ If it is non-zero, jump to fill1 to skip the following \ instruction LDA #&55 \ The fill byte is zero, i.e. colour 0 (black), so set \ A = &55, which is the value we use to represent \ colour 0 (black) in the screen buffer .fill1 STA V \ Store A in V, so V contains the correct fill byte for \ the screen buffer LDA #&7F \ Set T = &7F - topTrackLine SEC \ SBC topTrackLine \ We subtract this value from the start addresses for STA T \ the two dash data blocks that we are going to fill \ concurrently, and add the same value to the offset \ for the bottom line in VV (see the next instruction) \ \ There must be a reason for all these shenanigans, but \ it's currently eluding me ADC bottomTrackLine \ Set VV = T + bottomTrackLine STA VV \ We now calculate the start address of dash data block \ blockNumber - 1, which will be at: \ \ dashData + &80 * (blockNumber - 1) \ \ because the dash data blocks occur every &80 bytes \ from dashData \ \ We do this using the following simplification, where \ A = (blockNumber - 1) \ \ dashData + &80 * A \ = dashData + 256 / 2 * A \ = HI(dashData) << 8 + LO(dashData) + A << 7 \ \ LO(dashData) happens to be zero (as dashData = &3000), \ so we can keep going: \ \ = HI(dashData) << 8 + A << 7 \ = (HI(dashData) << 1 + A) << 7 \ = ((HI(dashData) << 1 + A) << 8) >> 1 \ \ In other words, if we build a 16-bit number with the \ high byte set to HI(dashData) << 1 + A, and then shift \ the whole thing right by one place, we have our result \ \ So this is the same as: \ \ ((HI(dashData) << 1 + blockNumber - 1) << 8) >> 1 \ \ We do this below, storing the 16-bit number in (Q A) LDA blockNumber \ Set A = blockNumber STA U \ Set U = blockNumber CLC \ Set A = A - 1 + HI(dashData) << 1 ADC #HI(dashData)<<1-1 \ = blockNumber - 1 + HI(dashData) << 1 \ \ so our 16-bit number is (A 0), and we want to shift \ right by one place LSR A \ Shift (A 0) right by 1, shifting bit 0 of A into the \ C flag STA Q \ Set Q = A, to store the high byte of the result in Q STA S \ Set S = A, to store the high byte of the result in S LDA #0 \ Shift the C flag into bit 7 of A, so A now contains ROR A \ the low byte of our result \ We now have our result in (Q A), which contains the \ start address of dash data block blockNumber - 1 \ We now subtract T, though as noted above, I'm unclear \ on the reason for this (but the maths all balances out \ in the end, so let's go with it) SEC \ Set (Q P) = (Q A) - T SBC T \ STA P \ We also set the C flag depending on the subtraction EOR #&80 \ Set (S R) = (Q P) - &80 STA R \ \ starting with the low bytes \ \ We can do the subtraction more efficiently by using \ EOR to flip between &xx00 and &xx80, as the dash data \ blocks always start at these addresses BPL fill2 \ We then decrement the high byte, but only if the EOR DEC S \ set the low byte to &80 rather than &00 (if we just \ set it to the latter, the BPL will skip the DEC) .fill2 BCS fill4 \ If the subtraction above didn't underflow, jump to \ fill4 to skip the next two instructions .fill3 DEC Q \ Otherwise decrement the high bytes in (Q P) and (S R) DEC S \ as the low byte subtraction underflowed .fill4 \ By this point, we have: \ \ * (Q P) points to the start address of dash data \ block blockNumber - 1, minus T \ \ * (S R) points to the start address of dash data \ block blockNumber - 2, minus T \ \ So if we fill block (Q P), we will be filling the \ block to the left of the edge, and if we fill block \ (S R), we will be filling the block further to the \ left \ We now enter a loop to fill the object, filling either \ one or both of these blocks at a time LDY U \ Set Y to the block number in U, which starts out as \ blockNumber and goes down by 2 on each loop iteration LDA fillDataOffset-1,Y \ Set A to entry Y - 1 from fillDataOffset, which gives \ us the offset of the bottom line of block Y - 1, \ i.e. blockNumber - 1, adjusted to ensure that filling \ to the left works properly DEY \ Set U = Y - 2 DEY \ = U - 2 STY U CMP bottomTrackLine \ If A < bottomTrackLine, then the bottom of the object BCC fill5 \ is below the bottom of the block, so jump to fill5 to \ do the fill from bottomTrackLine and up ADC T \ Set Y = A + T + C TAY \ = bottom line offset + T + 1 BPL fill6 \ If Y < &80, jump to fill6 to do the fill from track \ line Y and up, so the fill will start from address: \ \ (Q P) + Y = (Q P) + A + T + 1 \ = start address of (blockNumber - 1) - T \ + A + T + 1 \ = start address of (blockNumber - 1) \ + A + 1 \ \ i.e. from track line A in blockNumber - 1 \ If we get here then Y >= &80, which means: \ \ A + T + C >= &80 \ \ A + &7F - topTrackLine + 1 >= &80 \ \ A - topTrackLine >= 0 \ \ A >= topTrackLine \ \ so the bottom line offset is above the top track line, \ which is why we don't do the fill for these two blocks CPX #2 \ If X >= 2, jump to fill8 to move on to the next two BCS fill8 \ blocks to the left RTS \ Return from the subroutine .fill5 LDY VV \ Set Y = VV \ = T + bottomTrackLine \ \ so the fill will start from address: \ \ (Q P) + Y = (Q P) + T + bottomTrackLine \ = start address of (blockNumber - 1) - T \ + T + bottomTrackLine \ = start address of (blockNumber - 1) \ + bottomTrackLine \ \ i.e. from bottomTrackLine in blockNumber - 1 .fill6 \ We now do the fill, filling the relevant blocks from \ offset Y up to offset &7F, so that's: \ \ * From (Q P) + Y to (Q P) + &7F \ \ * And from (S R) + Y to (S R) + &7F if X >= 2 \ \ In the latter case we then reduce X by 2 and loop back \ to do the next two blocks LDA V \ Set A to the byte we want to fill with CPX #2 \ If X < 2, jump to fill9 to fill just one block and BCC fill9 \ return from the subroutine .fill7 STA (P),Y \ Fill the Y-th byte of (Q P) and (S R) with A STA (R),Y INY \ Increment Y to point to the next byte down the screen BPL fill7 \ Loop back until we have filled down to the &7F-th byte .fill8 DEX \ Set X = X - 2 DEX \ \ to move left by two blocks BNE fill3 \ If we haven't filled all the blocks, jump to fill3 to \ fill the next two blocks to the left RTS \ Return from the subroutine .fill9 STA (P),Y \ Fill the Y-th byte of (Q P) with A INY \ Increment Y to point to the next byte down the screen BPL fill9 \ Loop back until we have filled down to the &7F-th byte RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: GetColour (Part 1 of 3) \ Type: Subroutine \ Category: Screen buffer \ Summary: Calculate the colour of a specific pixel byte in the screen buffer \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ Y The track line number of the pixel byte to check \ \ blockNumber The dash data block number of the pixel byte to check \ \ (Q P) The address of the dash data block containing the pixel \ byte we want to check \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ A The colour of this pixel byte in the screen buffer \ \ ****************************************************************************** IF _ACORNSOFT OR _4TRACKS .GetColour CPY horizonLine \ If Y <= horizonLine then the byte we want to check is BCC gcol1 \ below the horizon, so jump to gcol1 to work out the BEQ gcol1 \ byte's colour LDA horizonLine \ Set A to the track line number of the horizon JSR SetMarker+3 \ Call SetMarker+3 to insert a marker value at the \ horizon line LDA colourPalette+1 \ Otherwise the byte is in the sky, so set A to logical \ colour 1 (blue) from the colour palette RTS \ Return from the subroutine .gcol1 LDA #0 \ Set T = 0, to use for storing the results of the STA T \ following comparisons LDA blockNumber \ Set A to the block number containing the pixel byte \ that we want to check CMP leftVergeStart,Y \ If A >= leftVergeStart for this track line, rotate a ROL T \ 1 into T, otherwise rotate a 0 into T CMP leftTrackStart,Y \ If A >= leftTrackStart for this track line, rotate a ROL T \ 1 into T, otherwise rotate a 0 into T CMP rightVergeStart,Y \ If A >= rightVergeStart for this track line, rotate a ROL T \ 1 into T, otherwise rotate a 0 into T CMP rightGrassStart,Y \ If A >= rightGrassStart for this track line, rotate a LDA T \ 1 into the result, otherwise rotate a 0, and copy the ROL A \ results from all four comparisons into A, so we have: \ \ * Bit 0: 1 if blockNumber >= rightGrassStart \ 0 if blockNumber < rightGrassStart \ \ * Bit 1: 1 if blockNumber >= rightVergeStart \ 0 if blockNumber < rightVergeStart \ \ * Bit 2: 1 if blockNumber >= leftTrackStart \ 0 if blockNumber < leftTrackStart \ \ * Bit 3: 1 if blockNumber >= leftVergeStart \ 0 if blockNumber < leftVergeStart BNE gcol7 \ If A is non-zero, then the block number containing the \ pixel we want to check is greater than at least one \ of the verge edges, so jump to gcol7 \ If we get here then blockNumber is less than all the \ track verge block numbers LDA backgroundColour,Y \ Set A to the background colour for the track line \ containing the pixel we want to check AND #%11101100 \ Extract the following bits: \ \ * Bits 5-7 and 2: records which routine set this \ colour \ \ * Bit 3: contains the verge type that was being \ drawn when this colour was set \ \ * 0 = leftVergeStart, rightVergeStart \ \ * 1 = leftTrackStart, rightGrassStart CMP #%01000000 \ If this colour matches these bits: BEQ gcol2 \ \ * Bits 5-7 and 2 = %010 0 \ \ * Bit 3 = 0 \ \ then the colour was set by UpdateBackground to the \ value in backgroundRight when drawing leftVergeStart \ or rightVergeStart, so jump to gcol2 CMP #%10001000 \ If this colour matches these bits: BEQ gcol2 \ \ * Bits 5-7 and 2 = %100 0 \ \ * Bit 3 = 1 \ \ then the colour was set by UpdateBackground to the \ value in backgroundLeft when drawing leftTrackStart \ or rightGrassStart, so jump to gcol2 CMP #%00000100 \ If this colour matches these bits: BEQ gcol2 \ \ * Bits 5-7 and 2 = %000 1 \ \ * Bit 3 = 0 \ \ then the colour was set by SetVergeBackground when \ drawing leftVergeStart or rightVergeStart, so jump to \ gcol2 LDA rightGrassStart,Y \ Set A to the block number containing the right edge of \ the right verge BPL gcol3 \ If A is positive then jump to gcol3 BMI gcol4 \ Jump to gcol4 to return the background colour for this \ track line as the pixel's colour (this BMI is \ effectively a JMP, as we just passed through a BPL) .gcol2 LDA backgroundColour,Y \ Set A to the background colour for the track line \ containing the pixel we want to check AND #%00010000 \ If bit 4 of the background colour is set, then the BNE gcol3 \ verge being drawn when the colour was set was \ rightVergeStart or rightGrassStart, so jump to gcol3 JSR gcol8 \ Call gcol8 to process the left verge JMP gcol4 \ Jump to gcol4 to return the background colour for this \ track line as the pixel's colour .gcol3 JSR gcol12 \ Call gcol12 to process the right verge .gcol4 LDA backgroundColour,Y \ Set A to the background colour for the track line \ containing the pixel we want to check AND #%00000011 \ Extract the colour number from bits 0-1 of A into X TAX LDA colourPalette,X \ Set A to logical colour X from the colour palette RTS \ Return from the subroutine .gcol5 LDA colourPalette \ Set A to logical colour 0 (black) from the colour \ palette RTS \ Return from the subroutine .gcol6 LDA colourPalette+3 \ Set A to logical colour 3 (green) from the colour \ palette RTS \ Return from the subroutine .gcol7 \ If we get here then the block number containing the \ pixel we want to check is greater than at least one \ of the verge edges, and we have the following: \ \ * Bit 0: 1 if blockNumber >= rightGrassStart \ 0 if blockNumber < rightGrassStart \ \ * Bit 1: 1 if blockNumber >= rightVergeStart \ 0 if blockNumber < rightVergeStart \ \ * Bit 2: 1 if blockNumber >= leftTrackStart \ 0 if blockNumber < leftTrackStart \ \ * Bit 3: 1 if blockNumber >= leftVergeStart \ 0 if blockNumber < leftVergeStart LSR A \ If blockNumber >= rightGrassStart, jump to gcol6 to BCS gcol6 \ return from the subroutine with the colour green LSR A \ If blockNumber >= rightVergeStart, jump to gcol12 BCS gcol12 LSR A \ If blockNumber >= leftTrackStart, jump to gcol5 to BCS gcol5 \ return from the subroutine with the colour black \ If we get here then blockNumber >= leftVergeStart, as \ we only jump to gcol7 if at least one of the four bits \ is set, so by a process of elimination, it must be \ bit 3 \ ****************************************************************************** \ \ Name: GetColour (Part 2 of 3) \ Type: Subroutine \ Category: Screen buffer \ Summary: Process the left verge \ \ ****************************************************************************** .gcol8 CPY vergeTopLeft \ If Y >= vergeTopLeft, jump to gcol6 to return from the BCS gcol6 \ subroutine with the colour green LDX leftSegment,Y \ Set X to the index within the track segment list of \ the segment for the left verge on this track line BMI gcol14 \ If bit 7 of X is set, then this entry in the \ rightSegment table was filled in by MapSegmentsToLines \ for a segment that doesn't have an entry in the track \ segment list, in which case the index of the last \ valid entry is captured in bits 0-6, so jump to gcol14 \ to clear bit 7 of X and return the colour of the verge \ mark for the segment beyond segment X JSR SetMarker \ Call SetMarker to insert a &AA marker into the screen \ buffer at the left verge .gcol9 LDA (P),Y \ If the current byte in the screen buffer is non-zero, BNE gcol11 \ then it is not empty, so jump to gcol11 LDA leftTrackStart,Y \ Set A to the block number containing the right edge of \ the left verge BMI gcol10 \ If bit 7 of A is set then the block number is still in \ its initialised form, so jump to gcol10 CMP blockNumber \ Set the C flag if A >= blockNumber, which contains the \ dash data block number for the current edge DEY \ Decrease the track line in Y BCS gcol9 \ If A >= blockNumber, loop back to gcol9 INY \ Increment the track line in Y .gcol10 JSR SetMarker+6 \ Call SetMarker+6 to insert a marker byte into the Y-th \ byte of the dash data block, but only if the Y-th \ entry is zero and blockOffset <= Y < V .gcol11 LDY V \ Set Y = V JMP gcol13 \ Jump to gcol13 to return the colour of the verge mark \ for the segment beyond segment X \ ****************************************************************************** \ \ Name: GetColour (Part 3 of 3) \ Type: Subroutine \ Category: Screen buffer \ Summary: Process the right verge \ \ ****************************************************************************** .gcol12 CPY vergeTopRight \ If Y >= vergeTopRight, jump to gcol6 to return from BCS gcol6 \ the subroutine with the colour green LDX rightSegment,Y \ Set X to the index within the track segment list of \ the segment for the right verge on this track line BMI gcol14 \ If bit 7 of X is set, then this entry in the \ rightSegment table was filled in by MapSegmentsToLines \ for a segment that doesn't have an entry in the track \ segment list, in which case the index of the last \ valid entry is captured in bits 0-6, so jump to gcol14 \ to clear bit 7 of X and return the colour of the verge \ mark for the segment beyond segment X JSR SetMarker \ Call SetMarker to insert a &AA marker into the screen \ buffer at the right verge .gcol13 LDA vergeDataRight-1,X \ Set A to entry X - 1 from vergeDataRight, which \ contains the colour of the verge for the segment \ beyond segment X AND #%00000011 \ Extract the colour number from bits 0-1 of A into X TAX LDA colourPalette,X \ Set A to logical colour X from the colour palette RTS \ Return from the subroutine .gcol14 TXA \ Clear bit 7 of X AND #%01111111 TAX BPL gcol13 \ Jump to gcol13 (this BPL is effectively a JMP as we \ just cleared bit 7 of A) ENDIF \ ****************************************************************************** \ \ Name: SetMarker \ Type: Subroutine \ Category: Screen buffer \ Summary: Insert a marker value into a dash data block \ \ ------------------------------------------------------------------------------ \ \ This routine inserts a marker value (&AA) into the current dash data block at \ a specified track line, but only if the current value in the dash data block \ is zero, and only if the track line is in the specified range. \ \ If the routine is called via SetMarker, then it works like this: \ \ * A is set to the pitch angle (track line) of the X-th entry in the verge \ buffer \ \ * The A-th byte in the dash data block at (Q P) is set to the marker &AA if \ blockOffset <= A < Y and it is currently zero \ \ If the routine is called via SetMarker+3, then it works like this: \ \ * The A-th byte in the dash data block at (Q P) is set to the marker &AA if \ blockOffset <= A < Y and it is currently zero \ \ If the routine is called via SetMarker+6, then it works like this: \ \ * The Y-th byte in the dash data block at (Q P) is set to the marker &AA if \ blockOffset <= Y < V and it is currently zero \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ Y A track line number \ \ X Index of an entry in the verge buffer (SetMarker only) \ \ ------------------------------------------------------------------------------ \ \ Other entry points: \ \ SetMarker+3 Use the value of A passed to the routine \ \ SetMarker+6 Use Y and V in place of A and Y \ \ ****************************************************************************** IF _ACORNSOFT OR _4TRACKS .SetMarker LDA yVergeRight,X \ Set A to the pitch angle of the X-th entry in the \ verge buffer (i.e. the track line number of the \ X-th entry) \ We join the subroutine here if we call SetMarker+3 STY V \ Set V to the track line number in Y TAY \ Set Y to the track line number in A \ We join the subroutine here if we call SetMarker+6 CPY V \ If Y >= V, jump to setm1 to return from the BCS setm1 \ subroutine with Y = V CPY blockOffset \ If Y < blockOffset, jump to setm1 to return from the BCC setm1 \ subroutine with Y = V LDA (P),Y \ If the Y-th byte in the dash data block is non-zero, BNE setm1 \ then it is not empty, so jump to setm1 to return from \ the subroutine with Y = V LDA #&AA \ Set the Y-th byte in the dash data block to &AA, to STA (P),Y \ act as a marker that gets picked up in the drawing \ routine .setm1 LDY V \ Restore Y to the track line number of the pixel byte \ to check, so it's unchanged by the call for calls to \ SetMarker and SetMarker+3 RTS \ Return from the subroutine ENDIF \ ****************************************************************************** \ \ Name: GetColourSup \ Type: Subroutine \ Category: Screen buffer \ Summary: Calculate the colour of a specific pixel byte in the screen buffer \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ Y The track line number of the pixel byte to check \ \ blockNumber The dash data block number of the pixel byte to check \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ A The colour of this pixel byte in the screen buffer \ \ ****************************************************************************** IF _SUPERIOR OR _REVSPLUS .GetColourSup CPY horizonLine \ If Y <= horizonLine then the byte we want to check is BCC scol1 \ below the horizon, so jump to scol1 to work out the BEQ scol1 \ byte's colour LDA colourPalette+1 \ Otherwise the byte is in the sky, so set A to logical \ colour 1 (blue) from the colour palette RTS \ Return from the subroutine .scol1 LDA blockNumber \ Set A to the block number containing the pixel byte \ that we want to check CMP rightGrassStart,Y \ If A >= rightGrassStart for this track line, then the BCS scol3 \ pixel byte is in the grass to the right of the track, \ so jump to scol3 to return colour 3 (green) CMP rightVergeStart,Y \ If A >= rightVergeStart for this track line, then the BCS scol5 \ pixel byte is on the right track verge, so jump to \ scol5 to work out its colour CMP leftTrackStart,Y \ If A >= leftTrackStart for this track line, then the BCS scol2 \ pixel byte is on the track, so jump to scol2 to return \ colour 0 (black) CMP leftVergeStart,Y \ If A >= leftVergeStart for this track line, then the BCS scol4 \ pixel byte is on the left track verge, so jump to \ scol4 to work out its colour LDA backgroundColour,Y \ If we get here then the byte is to the left of the \ left track verge, so set A to the background colour of \ this track line so we can extract the colour from bits \ 0-1 below BCC scol7 \ Jump to scol7 to return the pixel byte for the colour \ in A (this BCC is effectively a JMP as we just passed \ through a BCS) .scol2 LDA colourPalette \ Set A to logical colour 0 (black) from the colour \ palette RTS \ Return from the subroutine .scol3 LDA colourPalette+3 \ Set A to logical colour 3 (green) from the colour \ palette RTS \ Return from the subroutine .scol4 \ If we get here then the pixel byte is on the left \ track verge CPY vergeTopLeft \ If the track line in Y >= vergeTopLeft, jump to scol3 BCS scol3 \ to return colour 3 (green) LDA leftSegment,Y \ Set A to the index within the track segment list of \ the segment for the left verge on this track line JMP scol6 \ Jump to scol6 .scol5 \ If we get here then the pixel byte is on the right \ track verge CPY vergeTopRight \ If the track line in Y >= vergeTopRight, jump to scol3 BCS scol3 \ to return colour 3 (green) LDA rightSegment,Y \ Set A to the index within the track segment list of \ the segment for the right verge on this track line .scol6 AND #%01111111 \ Clear bit 7 of A TAX \ Set X to A LDA vergeDataRight-1,X \ Set A to entry X - 1 from vergeDataRight, which \ contains the colour of the verge mark for the segment \ beyond segment X .scol7 AND #%00000011 \ Extract the colour number from bits 0-1 of A into X TAX LDA colourPalette,X \ Set A to logical colour X from the colour palette RTS \ Return from the subroutine ENDIF \ ****************************************************************************** \ \ Name: AssistSteering \ Type: Subroutine \ Category: Tactics \ Summary: Apply computer assisted steering (CAS) when configured \ Deep dive: Tactics of the non-player drivers \ Computer assisted steering (CAS) \ \ ------------------------------------------------------------------------------ \ \ This routine applies computer assisted steering (CAS) to the joystick and \ keyboard, but only if it is enabled and we are already steering (if we are not \ steering, then there is no steering to assist). \ \ Jumps back to: \ \ * keys11 (joystick, CAS not enabled) \ \ * keys7 (joystick, CAS enabled, joystick is hardly steering) \ \ * keys10 (keyboard, or joystick with CAS applied) \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ (U T) The amount of steering currently being applied by the \ steering wheel: \ \ * For joystick, contains the scaled joystick \ x-coordinate as a sign-magnitude number with the \ sign in bit 0 (1 = left, 0 = right) \ \ * For keyboard, contains a signed 16-bit number, \ negative if bit 0 of steeringLo is set (left), \ positive if bit 0 of steeringLo is clear (right) \ \ (A T) Same as (U T) \ \ V For keyboard only: \ \ * V = 1 if ";" is being pressed (steer right) \ \ * V = 2 if "L" is being pressed (steer left) \ \ * V = 0 if neither is being pressed \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ A A is set to steeringLo \ \ (U T) The new amount of steering to apply, adjusted to add \ computer assisted steering, as a sign-magnitude number \ \ ------------------------------------------------------------------------------ \ \ Other entry points: \ \ AssistSteeringKeys For keyboard-controlled steering \ \ ****************************************************************************** IF _SUPERIOR OR _REVSPLUS .AssistSteering JSR GetSteeringAssist \ Set X = configAssist, set the C flag to bit 7 of \ directionFacing, and update the computer assisted \ steering (CAS) indicator on the dashboard BNE asst2 \ If CAS is enabled, jump to asst2 to skip the following \ instruction and apply CAS, otherwise we jump to keys11 \ to return to the ProcessDrivingKeys routine .asst1 JMP keys11 \ Return to the ProcessDrivingKeys routine at keys11 .asst2 BCS asst1 \ If bit 7 of directionFacing is set, then our car is \ facing backwards, so jump to asst1 to jump to keys11 \ in the ProcessDrivingKeys routine, as CAS only works \ when driving forwards CMP #5 \ If A >= 5, then the joystick is currently applying BCS asst4 \ some steering, so jump to asst4 to continue applying \ CAS \ Otherwise the joystick is not being used for steering \ at the moment, so there is no steering to assist and \ we don't apply CAS JMP keys7 \ Return to the ProcessDrivingKeys routine at keys7 to \ apply no joystick steering .AssistSteeringKeys JSR GetSteeringAssist \ Set X = configAssist, set the C flag to bit 7 of \ directionFacing, and update the CAS indicator on the \ dashboard BEQ asst3 \ If CAS is not enabled, jump to asst3 to set A and jump \ to keys10 in the ProcessDrivingKeys routine BCS asst3 \ If bit 7 of directionFacing is set, then our car is \ facing backwards, so jump to asst3 to jump to keys10 \ in the ProcessDrivingKeys routine, as CAS only works \ when driving forwards LDA V \ Set A = V BNE asst5 \ If A is non-zero, then one of the steering keys is \ being held down, so jump to asst5 to continue applying \ CAS \ Otherwise the keyboard is not being used for steering \ at the moment, so there is no steering to assist and \ we don't apply CAS .asst3 JMP asst13 \ Jump to asst13 to set A to steeringLo and return to \ the ProcessDrivingKeys routine at keys210 .asst4 \ If we get here then the joystick is being used for \ steering, and (A T) contains the scaled joystick \ x-coordinate as a sign-magnitude number with the \ sign in bit 0 (1 = left, 0 = right) LDA T \ Set the C flag to the inverse of the joystick EOR #1 \ x-coordinate's sign bit from bit 0 (i.e. 0 = left, LSR A \ 1 = right) LDA #3 \ Set A to 3 (if the C flag is set, i.e. right) or 2 (if SBC #0 \ the C flag is clear, i.e. left) .asst5 \ If we get here, then either the joystick or keyboard \ is being used for steering, and we have the following: \ \ * A = 2 if we are steering left \ \ We now set X as a flag for the steering direction, so \ we can use A for other purposes LDX #50 \ Set X = 50 to use as the value for steering left CMP #2 \ If A = 2 then we are steering left, so jump to asst6 BEQ asst6 \ to skip the following instruction LDX #10 \ Set X = 10 to use as the value for steering right \ So we now have the following that we can use to check \ which direction we are steering: \ \ * X = 10 if we are steering right \ \ * X = 50 if we are steering left .asst6 \ We now spend the rest of the routine calculating the \ amount of computer assisted steering (CAS) to apply, \ returning the result in the sign-magnitude number \ (U T) \ \ First, we set the following if we are steering right: \ \ (W V) = (steeringHi steeringLo) + 256 \ \ or the following if we are steering left: \ \ (W V) = (steeringHi steeringLo) - 256 \ \ by first converting (steeringHi steeringLo) from a \ sign-magnitude number to a signed 16-bit number and \ then doing the addition or subtraction LDA steeringLo \ Set V = steeringLo STA V LSR A \ Set the C flag to the sign of steeringLo LDA steeringHi \ Set A = steeringHi \ \ So (A V) = (steeringHi steeringLo) BCC asst7 \ If the C flag is clear then (steeringHi steeringLo) is \ positive, so jump to asst7 as (A V) already has the \ correct sign \ Otherwise (steeringHi steeringLo) is negative, so we \ need to negate (A V) LDA #0 \ Set (A V) = 0 - (A V) SEC \ SBC V \ starting with the low bytes STA V LDA #0 \ And then the high bytes SBC steeringHi .asst7 CLC \ Set (A V) = (A V) + 256 ADC #1 \ = (steeringHi steeringLo) + 256 CPX #50 \ If X <> 50, then we are steering right, so jump to BNE asst8 \ asst8 SBC #2 \ X = 50, so we are steering left, so set: \ \ (A V) = (A V) - 2 * 256 \ = (steeringHi steeringLo) + 256 - 2 * 256 \ = (steeringHi steeringLo) - 256 .asst8 STA W \ Set (W V) = (A V) \ \ So if we are steering right, we have: \ \ (W V) = (steeringHi steeringLo) + 256 \ \ and if we are steering left we have: \ \ (W V) = (steeringHi steeringLo) - 256 LDA xVergeRightLo,X \ Set (A T) = X-th (xVergeRightHi xVergeRightLo) - (W V) SEC \ SBC V \ starting with the low bytes STA T LDA xVergeRightHi,X \ And then the high bytes SBC W PHP \ Store the sign flag for X-th xVergeRight - (W V) on \ the stack, so we can retrieve it below JSR Absolute16Bit \ Set (A T) = |A T| \ = |X-th xVergeRight - (W V)| STA V \ Set (V T) = (A T) \ = |X-th xVergeRight - (W V)| LDY playerSegmentIndex \ Set Y to the index of the player's segment in the \ track segment buffer LDA #60 \ Set A = 60 - playerSpeedHi SEC SBC playerSpeedHi BPL asst9 \ If the result is positive, jump to asst9 to skip the \ following instruction LDA #0 \ Set A = 0, so A is always positive, and is zero if we \ are currently doing more than 60, so: \ \ A = max(0, 60 - playerSpeedHi) .asst9 ASL A \ Set U = 32 * A * 2 ADC #32 \ = 32 + max(0, 60 - playerSpeedHi) * 2 STA U \ \ So U is 32 if we are doing more than 60, and higher \ with lower speeds LDA segmentSteering,Y \ Fetch the carSteering value to steer round the corner \ for the player's track segment AND #%01111111 \ Zero the driving direction in bit 7 CMP #64 \ If A < 64, jump to asst10 to skip the following BCC asst10 \ instruction LDA #2 \ A >= 64, i.e. bit 6 is set, so set A = 2 .asst10 CMP #8 \ If A < 8, jump to asst11 to skip the following BCC asst11 \ instruction LDA #7 \ A >= 8, so set A = 7, i.e. A = min(A, 7) .asst11 \ By now A is between 0 and 7, and is set to 2 if bit 6 \ of segmentSteering was set ASL A \ Set A = A * 16 ASL A \ ASL A \ So A is in the range 0 to 112 ASL A CMP U \ If A < U, jump to asst12 to skip the following BCC asst12 \ instruction STA U \ A >= U, so set U = A, i.e. set U = max(U, A) .asst12 JSR Multiply8x16 \ Set (U T) = U * (V T) / 256 \ = U * |X-th xVergeRight - (W V)| / 256 LDA U \ Set (A T) = (U T) \ = U * |X-th xVergeRight - (W V)| / 256 PLP \ Retrieve the sign of the X-th xVergeRight - (W V) \ calculation that we stored above JSR Absolute16Bit \ Set the sign of (A T) to that of X-th xVergeRight - \ (W V), so we now have: \ \ (A T) = U * (X-th xVergeRight - (W V)) / 256 STA U \ Set (U T) = (A T) \ = U * (X-th xVergeRight - (W V)) / 256 LDA T \ Clear bit 0 of (U T) AND #%11111110 STA T LDA steeringLo \ Set the C flag to the sign in bit 0 of steeringLo LSR A BCS asst13 \ If the C flag is set, jump to asst13 to skip the \ following instruction JSR Negate16Bit+2 \ Set (A T) = -(U T) STA U \ Set (U T) = (A T) \ = -(U T) .asst13 LDA steeringLo \ Set A = steeringLo to return from the subroutine JMP keys10 \ Return to the ProcessDrivingKeys routine at keys10 ENDIF \ ****************************************************************************** \ \ Name: SetSteeringLimit \ Type: Subroutine \ Category: Keyboard \ Summary: Apply a maximum limit to the amount of steering \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ C flag The result of CMP steeringHi \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ (A T) A is set to |steeringHi steeringLo| \ \ ****************************************************************************** IF _SUPERIOR OR _REVSPLUS .SetSteeringLimit BCC slim1 \ Before calling this routine, we did a CMP steeringHi, \ so if A < steeringHi, jump to slim1 to return from \ the subroutine LDA steeringLo \ Set T = steeringLo with bit 0 cleared AND #%11111110 STA T LDA steeringHi \ Set A = steeringHi, so (A T) = (steeringHi steeringLo) \ with the sign bit in bit 0 cleared .slim1 RTS \ Return from the subroutine ENDIF \ ****************************************************************************** \ \ Name: SetPlayerDriftSup \ Type: Subroutine \ Category: Car geometry \ Summary: Record player drift, but only if the player is not in the first \ three segments of a track section \ \ ------------------------------------------------------------------------------ \ \ This routine is only present in the Superior Software release. \ \ In the Acornsoft release, this routine consists of a single ROR playerDrift \ instruction, so the Superior Software version differs as follows: \ \ * Acornsoft sets the playerDrift flag if A >= 22 \ \ * Superior sets the playerDrift flag if A >= 22 and objSectionSegmt for the \ player is >= 3 \ \ So the Superior version does not record drift in the first three segments of \ a new track section. \ \ ****************************************************************************** IF _SUPERIOR OR _REVSPLUS .SetPlayerDriftSup BCC drif1 \ If the C flag is clear, jump to drif1 to skip the \ following LDA objSectionSegmt,X \ Set A = objSectionSegmt, which keeps track of the \ player's segment number in the current track section CMP #3 \ If A < 3, clear the C flag, if A >= 3, set the C \ flag .drif1 ROR playerDrift \ Store the C flag in bit 7 of playerDrift, so this will \ be set if the original A >= 22 and if the second \ A >= 3 RTS \ Return from the subroutine NOP \ This instruction is unused, and is included to pad out \ the code ENDIF \ ****************************************************************************** \ \ Name: DrawObject \ Type: Subroutine \ Category: Drawing objects \ Summary: Draw an object of a specific type \ \ ------------------------------------------------------------------------------ \ \ This routine is used to draw objects such as road signs, corner markers and \ cars. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ objectType The type of object to draw (0 to 12) \ \ scaleUp The scale factor for this object (i.e. its size) \ \ colourPalette The colour palette to use for drawing the object \ \ X Driver number: \ \ * 0-19 = map logical colour 1 according to the driver \ number in X: \ \ * Drivers 0, 4, 8, 12, 16 map to colour 0 (black) \ \ * Drivers 1, 5, 9, 13, 17 map to colour 1 (red) \ \ * Drivers 2, 6, 10, 14, 18 map to colour 2 (white) \ \ * Drivers 2, 7, 11, 15, 19 map to colour 3 (green) \ \ * 20-22 = this is the four-object car, which is the \ closest car in front of us, so map logical \ colour 1 according to the number of the \ driver in front of us (using the same logic \ as above) \ \ * 23 = stick with the palette in colourPalette \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ colourPalette Gets reset back to the default palette \ \ ****************************************************************************** .DrawObject STX T \ Store the driver number in T \ We start by copying the four bytes from the standard \ colour palette in colourPalette to the object colour \ palette in objectPalette, as we use the latter to draw \ the object LDX #3 \ Set up a counter in X for copying the four palette \ bytes .dobj1 LDA colourPalette,X \ Copy the X-th byte of colourPalette to the X-th byte STA objectPalette,X \ of objectPalette DEX \ Decrement the loop counter BPL dobj1 \ Loop back until we have copied all four bytes LDA #%11110000 \ Map logical colour 2 in the colour palette to physical STA colourPalette+2 \ colour 1 (white in the track view), which sets it back \ to the default value \ \ This only has an effect when we call DrawObject from \ DrawCornerMarkers, which changes the value of colour 2 \ in colourPalette (all other calls to DrawObject leave \ the colour palette alone) \ We now set the palette differently, depending on the \ driver number in A: \ \ * 0-19 = map logical colour 1 to physical colour \ A mod 4 \ \ * 20-22 = map logical colour 1 to physical colour \ (number of the driver in front) mod 4 \ \ * 23 = don't change the palette, i.e. use the \ palette from colourPalette LDA T \ Set A = T, so A contains the driver number CMP #23 \ If A = 23, jump to dobj3 to skip the following palette BEQ dobj3 \ changes CMP #20 \ If A < 20, jump to dobj2 to map logical colour 1 to BCC dobj2 \ physical colour A mod 4, in other words: \ \ * Drivers 0, 4, 8, 12, 16 map to colour 0 (black) \ * Drivers 1, 5, 9, 13, 17 map to colour 1 (red) \ * Drivers 2, 6, 10, 14, 18 map to colour 2 (white) \ * Drivers 2, 7, 11, 15, 19 map to colour 3 (green) \ If we get here then A is 20, 21 or 22, which is the \ four-object car, so we map logical colour 1 to the \ number of the driver in front of us, mod 4 LDX positionAhead \ Set X to the position of the driver in front of us LDA driversInOrder,X \ Set A the number of the driver in front of us, so we \ map logical colour 1 to physical colour A mod 4 .dobj2 AND #3 \ Set X = A mod 4 TAX LDA colourPalette,X \ Map logical colour 1 in the object palette to logical STA objectPalette+1 \ colour X from the colour palette .dobj3 LDX #0 \ Set scaleDown = 0, so the object's scaffold is not STX scaleDown \ scaled down (as 2^scaleDown = 2^0 = 1) LDA scaleUp \ Set A = scaleUp, so A contains the object size, which \ we can also interpret as the object distance CMP horizonTrackWidth \ If A >= horizonTrackWidth, then the object is closer BCS dobj4 \ than the horizon line, so jump to dobj4 to skip the \ following instruction and set lowestTrackLine to 0 (so \ the whole object is drawn) LDX horizonLine \ Otherwise the object is further away than the horizon \ line, so set X to the track line number of the \ horizon, so the parts of the object below this line do \ not get drawn (as they are below the horizon line, so \ presumably hidden by a hill) .dobj4 STX lowestTrackLine \ Set lowestTrackLine = X, so the object gets cut off at \ the horizon line when scaleUp < horizonTrackWidth CMP #64 \ If A >= 64, i.e. scaleUp >= 64, jump to dobj5 to skip BCS dobj5 \ the following \ Otherwise we can alter the values of scaleUp and \ scaleDown to be more accurate but without fear of \ overflow, by multiplying both scale factors by 4 \ (as we know 4 * scaleUp is < 256) ASL A \ Set scaleUp = A * 4 ASL A \ = scaleUp * 4 STA scaleUp LDA #2 \ Set scaleDown = 2, so the object's scaffold is scaled STA scaleDown \ down by 2^scaleDown = 2^2 = 4 \ \ So the overall scaling of the scaffold is the same, \ but we retain more accuracy .dobj5 LDX objectType \ Set X to the type of object we're going to draw \ If the object type is 10, 11 or 12, then it's one of \ the turn signs (chicane, left or right turn), so we \ draw this as two objects, starting with a blank white \ sign (object type 9) and then the sign contents \ (object 10, 11 or 12) \ \ We only draw the sign contents if our car is facing \ forwards, so the back of the sign is blank CPX #10 \ If X < 10, jump to dobj6 to skip the following BCC dobj6 \ instruction LDX #9 \ Set X = 9, so we first draw an object of type 9 for \ the blank white sign, before drawing another object of \ type objectType .dobj6 STX thisObjectType \ Store X in thisObjectType, so we can check it again \ below in case we need to draw two objects LDA scaffoldIndex,X \ Set QQ to the index of the first scaffold entry in STA QQ \ objectScaffold for object type X LDA scaffoldIndex+1,X \ Set II to the index of the first scaffold entry in STA II \ objectScaffold for object type X + 1 (so the last \ entry for object type X will be index II - 1) LDA objectIndex,X \ Set QQ to the index of the first entry in the object STA MM \ data tables for object type X (so MM will point to the \ first entry for this object in the objectTop, \ objectBottom, objectLeft, objectRight and objectColour \ tables) JSR ScaleObject \ Scale the object's scaffold by the scaleUp and \ scaleDown factors, storing the results in the \ scaledScaffold table BCS dobj7 \ If the call to ScaleObject set the C flag then the \ scaling process overflowed, in which case we do not \ draw the object, so jump to dobj7 to return from the \ subroutine JSR DrawObjectEdges \ Draw the scaled object in the screen buffer by drawing \ all the object's edges LDX objectType \ Set X to the type of object we are drawing, in case we \ need to draw a second object LDA thisObjectType \ If the object we just drew is not an object of type 9, CMP #9 \ then this is not a two-part road sign object, so jump BNE dobj7 \ to dobj7 to return from the subroutine \ Otherwise we just drew an object of type 9, for the \ blank white sign, so now we draw a second object for \ the sign's contents, but only if our car is facing \ forwards (if we are facing backwards, then we see the \ back of the sign, which is blank) LDA directionFacing \ If bit 7 of directionFacing is clear, then our car is BPL dobj6 \ facing forwards, so loop back to draw the contents of \ the sign in object type objectType .dobj7 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: ScaleObject \ Type: Subroutine \ Category: Drawing objects \ Summary: Scale an object's scaffold by the scale factors in scaleUp and \ scaleDown \ Deep dive: Scaling objects with scaffolds \ \ ------------------------------------------------------------------------------ \ \ This routine is used when drawing objects such as road signs, corner markers \ and cars. \ \ It takes the values from the objectScaffold table, which contain an object's \ scaffold (i.e. all the essential measurements that we need to build the \ object), and scales them according to the values of scaleUp and scaleDown. \ \ As only scaffold measurements are used when drawing an object, this routine \ scales the whole object, according to the two scale factors. \ \ The value in scaleUp is the numerator of the scale factor, which scales the \ scaffold up, so bigger values of scaleUp give bigger objects. \ \ The value in scaleDown is the denominator of the scale factor, which scales \ the scaffold down, so bigger values of scaleDown give smaller objects. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ QQ Index of the first objectScaffold entry for this object \ \ II Index of the last objectScaffold entry for this object \ (where the last entry is index II - 1) \ \ scaleUp Numerator scale factor \ \ scaleDown Denominator scale factor \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ C flag Denotes whether the scaling was successful: \ \ * Clear if we manage to scale the scaffold \ \ * Set if the scaling of any individual scaffold \ measurements overflows, in which case we do not draw \ the object \ \ scaledScaffold The scaled scaffold \ \ scaledScaffold+8 The scaled scaffold, with each measurement negated \ \ ****************************************************************************** .ScaleObject LDA scaleUp \ Set scaleRange = scaleUp STA scaleRange LSR A \ Set scaleRange+1 = scaleUp >> 1 STA scaleRange+1 \ = scaleUp / 2 LSR A \ Set scaleRange+2 = scaleUp >> 2 STA scaleRange+2 \ = scaleUp / 4 LSR A \ Set scaleRange+3 = scaleUp >> 3 STA scaleRange+3 \ = scaleUp / 8 LSR A \ Set scaleRange+4 = scaleUp >> 4 STA scaleRange+4 \ = scaleUp / 16 LSR A \ Set scaleRange+5 = scaleUp >> 5 STA scaleRange+5 \ = scaleUp / 32 \ So scaleRange + n contains scaleUp / 2^n LDY QQ \ We now loop through the objectScaffold table from \ entry QQ to entry II - 1, so set a loop counter in Y \ to act as an index LDX #0 \ Set W = 0, to be used as an index as we populate the STX W \ scaledScaffold table, incrementing by one byte for \ each loop .prep1 LDA objectScaffold,Y \ Set A to the Y-th scaffold measurement BPL prep2 \ If bit 7 of A is clear, jump to prep2 to do the \ calculation that only uses bits 0-2 of A \ If we get here, bit 7 of A is set, so now we do the \ following calculation, where the value of A from the \ objectScaffold table is %1abbbccc: \ \ A = a * scaleUp/2 + scaleUp/2^b-2 + scaleUp/2^c-2 \ --------------------------------------------- \ 2^scaleDown \ \ = scaleUp * (a/2 + 1/2^b-2 + 1/2^c-2) \ ----------------------------------- \ 2^scaleDown \ \ scaleUp \ = ----------- * (a/2 + 1/2^b-2 + 1/2^c-2) \ 2^scaleDown \ \ scaleUp \ = ----------- * scaffold \ 2^scaleDown \ \ We then store this as the next entry in scaledScaffold \ \ Note that b and c are always in the range 3 to 7, so \ they look up the values we stored in scaleRange above AND #%00000111 \ Set X = bits 0-2 of A TAX \ = %ccc \ = c LDA scaleRange-2,X \ Set T = entry X-2 in scaleRange STA T \ = scaleUp / 2^X-2 \ = scaleUp / 2^c-2 LDA objectScaffold,Y \ Set A to the Y-th scaffold measurement STA U LSR A \ Set X = bits 3-5 of A LSR A \ = %bbb LSR A \ = b AND #%00000111 TAX LDA scaleRange-2,X \ Set A = entry X-2 in scaleRange + T CLC \ = scaleUp / 2^X-2 + scaleUp / 2^c-2 ADC T \ = scaleUp / 2^b-2 + scaleUp / 2^c-2 BIT U \ If bit 6 of U is clear, jump to prep3 BVC prep3 CLC \ If bit 6 of U is set: ADC scaleRange+1 \ \ A = A + scaleRange+1 \ = A + scaleUp / 2 JMP prep3 \ Jump to prep3 .prep2 \ If we get here, bit 7 of the Y-th objectScaffold is \ clear, so we do the following calculation, where \ A is %00000ccc: \ \ A = scaleUp / 2^c-2 \ --------------- \ 2^scaleDown \ \ = scaleUp * 1/2^c-2 \ ----------------- \ 2^scaleDown \ \ = scaleUp \ ----------- * 1/2^c-2 \ 2^scaleDown \ \ scaleUp \ = ----------- * scaffold \ 2^scaleDown \ \ We then store this as the next entry in scaledScaffold TAX \ Set A = entry c-2 in scaleRange LDA scaleRange-2,X \ = scaleUp / 2^c-2 .prep3 LDX scaleDown \ If scaleDown = 0 then the scale factor is 2^scaleDown BEQ prep5 \ = 2^0 = 1, so jump to prep5 to skip the division \ We now shift A right by X places, which is the same as \ dividing by 2^X = 2^scaleDown .prep4 LSR A \ Set A = A >> 1 DEX \ Decrement the shift counter BNE prep4 \ Loop back until we have shifted A right by X places, \ and the C flag contains the last bit shifted out from \ bit 0 of A ADC #0 \ Set A = A + C to round the result of the division to \ the nearest integer .prep5 LDX W \ Set X to W, the index into the tables we are building STA scaledScaffold,X \ Store A in the X-th byte of scaledScaffold EOR #&FF \ Set A = ~A BPL prep6 \ If bit 7 of A is clear, i.e. it was set before the \ EOR, then the result of the scaling was >= 128, which \ is an overflow of the scaling \ \ If the scaling overflows, then the object is too big \ to be drawn, so we jump to prep6 to return from the \ subroutine with the C flag set, so we do not draw this \ object and ignore all the values calculated here CLC \ Store -A in the X-th byte of scaledScaffold+8 ADC #1 STA scaledScaffold+8,X INC W \ Increment the index counter INY \ Increment the loop counter CPY II \ Loop back until Y has looped through QQ to II - 1 BNE prep1 CLC \ Clear the C flag to indicate a successful scaling RTS \ Return from the subroutine .prep6 SEC \ Set the C flag to indicate that scaling overflowed and \ the object should not be drawn RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: DrawObjectEdges \ Type: Subroutine \ Category: Drawing objects \ Summary: Draw all the parts of an object by drawing edges into the screen \ buffer \ Deep dive: Creating objects from edges \ \ ------------------------------------------------------------------------------ \ \ This routine is used to draw road signs, corner markers and cars. They are \ drawn as edges - specifically the left and right edges - into the screen \ buffer in the dash data blocks. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ MM The index of the first entry in the object data tables \ for this this object (i.e. the index of the data for the \ object's first part) \ \ xPixelCoord The pixel x-coordinate of the centre of the object \ \ yPixelCoord The object's y-coordinate (for the centre of the object) \ in terms of track lines, so 80 is the top of the track \ view and 0 is the bottom of the track view \ \ lowestTrackLine Hide any part of the object that's below the specified \ track line (typically used to stop an object from being \ drawn below the horizon) \ \ * 0 = draw the whole object \ \ * Non-zero = only draw the part of the object that's \ above this track line \ \ ****************************************************************************** .DrawObjectEdges LDY MM \ Set Y to the index of this object data in the object \ data tables \ We now work our way through the data for this object, \ drawing one part at a time, using thisObjectIndex and \ Y as the loop counter as we loop through each part \ \ Note that most object parts are defined by one set of \ object data, so they correspond to two edges (left and \ right), but object types 2 and 4 contain four-edge \ object parts, which are defined by two sets of data, \ and therefore two loop iterations .drob1 LDA colourPalette \ Set rightOfEdge to logical colour 0 in the standard STA rightOfEdge \ colour palette, so the fill colour to the left of the \ first edge is set to a default of black when we call \ DrawObjectEdge below LDA #0 \ Set prevEdgeInByte = 0 to indicate that the first edge STA prevEdgeInByte \ is not sharing a pixel byte with the previous edge (as \ there isn't a previous edge) STA edgePixelMask \ Set edgePixelMask = 0 to pass to DrawObjectEdge below \ as there is no previous edge, so there should be no \ mask for the previous edge in the same pixel byte LDX objectTop,Y \ Set A to the scaled scaffold for the top of this part LDA scaledScaffold,X \ of the object CLC \ Set A = A + yPixelCoord ADC yPixelCoord \ \ so A is now the track line of the top of the object BMI drob9 \ If A > 128, then the top of this object part is well \ above the track view, so jump to drob9 to move on to \ the next object part as this one doesn't fit on-screen CMP #80 \ If A >= 80, set A = 79, as the maximum track line at BCC drob2 \ the very top of the track view is 79 LDA #79 .drob2 STA topTrackLine \ Store A in N as the number of the top track line, to \ send to DrawObjectEdge below LDX objectBottom,Y \ Set A to the scaled scaffold for the bottom of this LDA scaledScaffold,X \ part of the object CLC \ Set A = A + yPixelCoord ADC yPixelCoord \ \ so A is now the track line of the bottom of the object BMI drob3 \ If A < 0, then the bottom of this object part is lower \ than the bottom of the track view, so jump to drob3 to \ set A = lowestTrackLine, so we only draw the object \ down to the lowest line allowed CMP lowestTrackLine \ If A >= lowestTrackLine, jump to drob4 to skip the BCS drob4 \ following .drob3 \ If we get here then either the bottom track line in A \ is negative or A < lowestTrackLine, both of which are \ below the lowest level that we want to draw, so we \ cut off the bottom of the object to fit LDA lowestTrackLine \ Set A = lowestTrackLine, so the minimum track line \ number is set to lowestTrackLine and we only draw the \ object down to the lowest line allowed NOP \ These instructions have no effect - presumably they NOP \ are left over from changes during development .drob4 CMP topTrackLine \ If A >= N, then the bottom track line for this object BCS drob9 \ in A is higher than the top track line in N, so jump \ to drob9 to move on to the next object part as there \ is nothing to draw for this part \ We now set up the parameters to pass to DrawObjectEdge \ below, to draw the left and right edges STA bottomTrackLine \ Set bottomTrackLine = A as the bottom track line LDX objectLeft,Y \ Set thisEdge to the scaled scaffold for the left edge LDA scaledScaffold,X \ of this part of the object, to pass to DrawObjectEdge STA thisEdge LDX objectRight,Y \ Set nextEdge to the scaled scaffold for the right LDA scaledScaffold,X \ edge of this part of the object, to pass to STA nextEdge \ DrawObjectEdge LDA objectColour,Y \ Set A to the colour data for this object part STA colourData \ Set colourData to the colour data for this object part STY thisObjectIndex \ Store the current index into the object data in \ thisObjectIndex LDY #1 \ Draw the left edge of this object part JSR DrawObjectEdge .drob5 BIT colourData \ If bit 7 is set in the colour data for this object BMI drob10 \ part, then this is a four-edge object part, so \ jump to drob10 to draw the extra two edges before \ returning here (with bit 7 of colourData clear) to \ draw the fourth edge LDA #0 \ Set A = 0 to send to DrawObjectEdge as the fill colour \ to the right, as there is no fill to the right of the \ object LDY #2 \ Draw the right edge of this object part JSR DrawObjectEdge BIT colourData \ If bit 6 is set in the colour data for this object BVS drob7 \ part, then this indicates that this is the last part \ of this object, so jump to drob7 to return from the \ subroutine as we have now drawn the whole object LDY thisObjectIndex \ Otherwise we need to move on to the next part, so set \ Y to the loop counter .drob6 INY \ Increment the loop counter to point to the data for \ the next object part JMP drob1 \ Loop back to drob1 to process the next object part .drob7 RTS \ Return from the subroutine .drob8 \ We get here when we come across data that forms the \ second and third stages of a four-edge object part, \ so we now need to skip that data as we have already \ processed it AND #%01000000 \ If bit 6 of A is set, i.e. 64 + x, jump to drob7 to BNE drob7 \ return from the subroutine, as we have just drawn the \ last part of the object we wanted to draw INY \ Increment the loop counter to point to the data for \ the next object part .drob9 LDA objectColour,Y \ Set A to the colour data for this object part BMI drob8 \ If bit 7 of A is set, i.e. 128 + x, jump to drob8 to \ skip this bit of data and move on to the next, as this \ contains the data for the second and third edges of a \ four-edge object part, and this will already have \ been processed in drob10 AND #%01000000 \ If bit 6 of A is set, i.e. 64 + x, jump to drob7 to BNE drob7 \ return from the subroutine, as we have just drawn the \ last part of the object we wanted to draw BEQ drob6 \ Jump to drob6 to move on to the next object part (this \ BEQ is effectively a JMP as we just passed through a \ BNE) .drob10 \ If we get here then the colour data for this object \ part has bit 7 set, so this is a four-edge object \ part and we need to draw the second and third edges \ \ The second and third edges are defined in the next bit \ of object data, as follows: \ \ * Second edge: nextEdge = objectLeft \ colourData = objectRight \ \ * Third edge: nextEdge = objectTop \ colourData = objectColour LDY thisObjectIndex \ Set Y to the loop counter INY \ Increment the loop counter to point to the next bit of STY thisObjectIndex \ object data (which contains the data for the second \ and third edges) LDX objectLeft,Y \ Set nextEdge to the scaled data from objectLeft for LDA scaledScaffold,X \ this object part, to pass to DrawObjectEdge STA nextEdge LDA objectRight,Y \ Set colourData to the data from objectRight for this STA colourData \ object part, to pass to DrawObjectEdge LDY #0 \ Draw the second edge of the four-edge object part JSR DrawObjectEdge LDY thisObjectIndex \ Set Y to the index into the object data LDX objectTop,Y \ Set nextEdge to the scaled data from objectTop for LDA scaledScaffold,X \ this object part, to pass to DrawObjectEdge STA nextEdge LDA objectColour,Y \ Set colourData to the data from objectColour for this STA colourData \ object part, to pass to DrawObjectEdge LDY #0 \ Draw the third edge of the four-edge object part JSR DrawObjectEdge JMP drob5 \ Loop back to drob5 to draw the fourth edge, with \ colourData set to the colour data from the third edge, \ which does not have bit 7 set \ ****************************************************************************** \ \ Name: GetObjYawAngle (Part 1 of 4) \ Type: Subroutine \ Category: 3D objects \ Summary: Calculate an object's yaw angle \ Deep dive: Pitch and yaw angles \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The offset of the variable to use for the object's 3D \ coordinates \ \ * &F4 = xHelmetCoord \ \ * &FA = xCoord1 \ \ * &FD = xCoord2 \ \ Y The offset of the second variable to use: \ \ * 0 = xPlayerCoord \ \ * 6 = xRoadSignCoord \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ (JJ II) The yaw angle of the object \ \ (J I) max(|x-delta|, |z-delta|) \ \ (H G) min(|x-delta|, |z-delta|) \ \ M The smaller yaw angle of the object, where 0 to 255 \ represents 0 to 45 degrees \ \ X X is preserved \ \ ------------------------------------------------------------------------------ \ \ Other entry points: \ \ GetObjYawAngle-2 Use xPlayerCoord (Y = 0) \ \ ****************************************************************************** LDY #0 \ Use xPlayerCoord for the second variable when calling \ the routine via GetObjYawAngle-2 .GetObjYawAngle \ The vectors used in this routine are configured by the \ values of X and Y, but for the purposes of simplicity, \ the comments will assume the following: \ \ * X = &FD, xCoord2 \ \ * Y = 0, xPlayerCoord LDA xSegmentCoordILo,X \ Set (VV PP) = xCoord2 - xPlayerCoord SEC \ SBC xPlayerCoordHi,Y \ starting with the low bytes STA PP LDA xSegmentCoordIHi,X \ And then the high bytes SBC xPlayerCoordTop,Y STA VV \ Let's call this difference in x-coordinates x-delta, \ so: \ \ (VV PP) = x-delta BPL rotn1 \ If (VV PP) is positive, jump to rotn1 to skip the \ following LDA #0 \ Set (VV PP) = 0 - (VV PP) SEC \ SBC PP \ starting with the low bytes STA PP LDA #0 \ And then the high bytes SBC VV \ So (VV PP) is now positive, in other words: \ \ (VV PP) = |x-delta| .rotn1 STA SS \ Set (SS PP) = (VV PP) \ = |x-delta| LDA zSegmentCoordILo,X \ Set (GG RR) = zCoord2 - zPlayerCoord SEC \ SBC zPlayerCoordHi,Y \ starting with the low bytes STA RR LDA zSegmentCoordIHi,X \ And then the high bytes SBC zPlayerCoordTop,Y STA GG \ Let's call this difference in z-coordinates z-delta, \ so: \ \ (GG RR) = z-delta BPL rotn2 \ If (GG RR) is positive, jump to rotn2 to skip the \ following LDA #0 \ Set (GG RR) = 0 - (GG RR) SEC \ SBC RR \ starting with the low bytes STA RR LDA #0 \ And then the high bytes SBC GG \ So (GG RR) is now positive, in other words: \ \ (GG RR) = |z-delta| .rotn2 STA UU \ Set (UU RR) = (GG RR) \ = |z-delta| \ At this point we have the following: \ \ (SS PP) = |x-delta| \ \ (UU RR) = |z-delta| \ \ We now compare these two 16-bit values, starting with \ the high bytes, and then the low bytes (if the high \ bytes are the same) CMP SS \ If UU < SS, then (UU RR) < (SS PP), so jump to rotn3 BCC rotn3 BNE rotn4 \ If UU <> SS, i.e. UU > SS, then (UU RR) > (SS PP), so \ jump to rotn4 with the C flag clear \ The high bytes are equal, so now we compare the low \ bytes LDA RR \ If RR >= PP, then (UU RR) >= (SS PP), so jump to rotn4 CMP PP \ with the C flag set BCS rotn4 \ Otherwise (UU RR) < (SS PP), so fall through into \ rotn3 .rotn3 \ If we get here then (UU RR) < (SS PP), so: \ \ |z-delta| < |x-delta| LDA UU \ Set (H G) = (UU RR) STA H \ = |z-delta| LDA RR \ STA G \ and (H G) contains the smaller value LDA PP \ Set (J I) = (SS PP) STA I \ = |x-delta| LDA SS \ STA J \ and (J I) contains the larger value JMP rotn6 \ Jump to rotn6 .rotn4 \ If we get here then (UU RR) >= (SS PP), so: \ \ |z-delta| >= |x-delta| PHP \ Store the status flags on the stack, and in particular \ the Z flag, which will be set if the two match, \ i.e. if |z-delta| = |x-delta| \ \ In other words, a BEQ would branch with these flags LDA SS \ Set (H G) = (SS PP) STA H \ = |x-delta| LDA PP \ STA G \ and (H G) contains the smaller value LDA RR \ Set (J I) = (UU RR) STA I \ = |z-delta| LDA UU \ STA J \ and (J I) contains the larger value PLP \ Retrieve the status flags we stored above BEQ rotn9 \ If (UU RR) = (SS PP), jump to rotn9 JMP rotn14 \ Jump to rotn14 \ ****************************************************************************** \ \ Name: GetObjYawAngle (Part 2 of 4) \ Type: Subroutine \ Category: 3D objects \ Summary: Calculate yaw angle for when |x-delta| > |z-delta| \ Deep dive: Pitch and yaw angles \ \ ****************************************************************************** .rotn5 \ This part is called from below, if we want to scale \ the division ASL RR \ Set (UU RR) = (UU RR) << 1 ROL UU .rotn6 \ If we get here, then: \ \ * (J I) = (A PP) = |x-delta| \ \ * VV is the high byte of x-delta \ \ * (H G) = (UU RR) = |z-delta| \ \ * GG is the high byte of z-delta \ \ * |x-delta| > |z-delta| \ \ We now do the following division so we can use \ trigonometry to calculate the yaw angle: \ \ |z-delta| / |x-delta| \ \ To get started, we shift both 16-bit values to the \ left as far as possible, which we can do without \ affecting the result as we are going to divide the two \ values, so any mutual shifts will cancel each other \ out in the division \ \ Once that's done, we can drop the low bytes and just \ divide the high bytes, which retains as much accuracy \ as possible while avoiding the need for full 16-bit \ division \ \ So we keep shifting left until we get a 1 in bit 7 of \ (A PP), as that's the larger of the two values ASL PP \ Set (A PP) = (A PP) << 1 ROL A BCC rotn5 \ If we just shifted a 0 out of the high byte of (A PP), \ then we can keep shifting, so loop back to rotn6 to \ keep shifting both values ROR A \ We just shifted a 1 out of bit 7 of A, so reverse the \ shift so A contains the correct high byte (we don't \ care about the low byte any more) \ So by this point, (A PP) and (UU RR) have both been \ scaled by the same number of shifts STA V \ Set V = A, the high byte of the scaled |x-delta| LDA RR \ Set T = RR, the low byte of the scaled |z-delta|, to STA T \ use for rounding the result in Divide8x8 LDA UU \ Set A = UU, the high byte of the scaled |z-delta| CMP V \ If A = V then the high bytes of the scaled values BEQ rotn9 \ match, so jump to rotn9, which deals with the case \ when the xVector and zVector values are equal \ We have scaled both values, so now for the division of \ the high bytes JSR Divide8x8 \ Set T = 256 * A / V \ = 256 * |z-delta| / |x-delta| \ \ using the lower byte of the |z-delta| numerator for \ rounding LDA #0 \ Set II = 0 to use as the low byte for the final yaw STA II \ angle LDY T \ Set A = arctanY(T) LDA arctanY,Y \ = arctanY(|z-delta| / |x-delta|) \ \ So this is the yaw angle of the object STA M \ Store the yaw angle in M, to return from the \ subroutine LSR A \ Set (JJ II) = (A 0) >> 3 ROR II \ = A * 256 / 8 LSR A \ = A * 32 ROR II \ = arctanY(|z-delta| / |x-delta|) * 32 LSR A ROR II STA JJ LDA VV \ If VV and GG have different signs, then so do x-delta EOR GG \ and z-delta, so jump to rotn7 BMI rotn7 LDA #0 \ Negate (JJ II) SEC \ SBC II \ starting with the low bytes STA II LDA #0 \ And then the high bytes SBC JJ STA JJ .rotn7 LDA #64 \ Set A = 64, to add to the high byte below BIT VV \ If x-delta is positive, jump to rotn8 to skip the BPL rotn8 \ following instruction \ If we get here then x-delta is negative LDA #&C0 \ Set A = -64, to add to the high byte below .rotn8 CLC \ Set (JJ II) = (JJ II) + (A 0) ADC JJ \ STA JJ \ which is one of the following: \ \ (JJ II) = (JJ II) + 64 * 256 \ \ (JJ II) = (JJ II) - 64 * 256 \ \ depending on the sign of x-delta RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: GetObjYawAngle (Part 3 of 4) \ Type: Subroutine \ Category: 3D objects \ Summary: Calculate yaw angle for when |x-delta| = |z-delta| \ Deep dive: Pitch and yaw angles \ \ ****************************************************************************** .rotn9 \ If we get here, then: \ \ * VV is the high byte of x-delta \ \ * GG is the high byte of z-delta \ \ * |x-delta| = |z-delta| LDA #255 \ Set M = 255, to represent a yaw angle of 45 degrees STA M LDA #0 \ Set II = 0 to use as the low byte for the final yaw STA II \ angle BIT VV \ If x-delta is positive, jump to rotn11 BPL rotn11 \ If we get here then x-delta is negative BIT GG \ If z-delta is positive, jump to rotn10 BPL rotn10 \ If we get here then both x-delta and z-delta are \ negative LDA #&A0 \ Set (JJ II) = -96 * 256 STA JJ RTS \ Return from the subroutine .rotn10 \ If we get here then x-delta is negative and y-delta \ is positive LDA #&E0 \ Set (JJ II) = -32 * 256 STA JJ RTS \ Return from the subroutine .rotn11 \ If we get here then x-delta is positive BIT GG \ If z-delta is positive, jump to rotn12 BPL rotn12 \ If we get here then x-delta is positive and y-delta \ is negative LDA #&60 \ Set (JJ II) = 96 * 256 STA JJ RTS \ Return from the subroutine .rotn12 \ If we get here then both x-delta and z-delta are \ positive LDA #&20 \ Set (JJ II) = 32 * 256 STA JJ RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: GetObjYawAngle (Part 4 of 4) \ Type: Subroutine \ Category: 3D objects \ Summary: Calculate yaw angle for when |x-delta| < |z-delta| \ Deep dive: Pitch and yaw angles \ \ ****************************************************************************** .rotn13 \ This part is called from below, if we want to scale \ the division ASL PP \ Set (SS PP) = (SS PP) << 1 ROL SS .rotn14 \ If we get here, then: \ \ * (H G) = (SS PP) = |x-delta| \ \ * VV is the high byte of x-delta \ \ * (J I) = (A RR) = |z-delta| \ \ * GG is the high byte of z-delta \ \ * |x-delta| < |z-delta| \ \ We now do the following division so we can use \ trigonometry to calculate the yaw angle: \ \ |x-delta| / |z-delta| \ \ To get started, we shift both 16-bit values to the \ left as far as possible, which we can do without \ affecting the result as we are going to divide the two \ values, so any mutual shifts will cancel each other \ out in the division \ \ Once that's done, we can drop the low bytes and just \ divide the high bytes, which retains as much accuracy \ as possible while avoiding the need for full 16-bit \ division \ \ So we keep shifting left until we get a 1 in bit 7 of \ (A RR), as that's the larger of the two values ASL RR \ Set (A RR) = (A RR) << 1 ROL A BCC rotn13 \ If we just shifted a 0 out of the high byte of (A RR), \ then we can keep shifting, so loop back to rotn13 to \ keep shifting both values ROR A \ We just shifted a 1 out of bit 7 of A, so reverse the \ shift so A contains the correct high byte (we don't \ care about the low byte any more) \ So by this point, (A RR) and (SS PP) have both been \ scaled by the same number of shifts STA V \ Set V = A, the high byte of the scaled |z-delta| LDA PP \ Set T = PP, the low byte of the scaled |x-delta|, to STA T \ use for rounding the result in Divide8x8 LDA SS \ Set A = SS, the high byte of the scaled |x-delta| CMP V \ If A = V then the high bytes of the scaled values BEQ rotn9 \ match, so jump to rotn9, which deals with the case \ when the xVector and zVector values are equal \ We have scaled both values, so now for the division of \ the high bytes JSR Divide8x8 \ Set T = 256 * A / V \ = 256 * |x-delta| / |z-delta| \ \ using the lower byte of the |x-delta| numerator for \ rounding LDA #0 \ Set II = 0 to use as the low byte for the final yaw STA II \ angle LDY T \ Set A = arctanY(T) LDA arctanY,Y \ = arctanY(|x-delta| / |z-delta|) \ \ So this is the yaw angle of the object STA M \ Store the yaw angle in M, to return from the \ subroutine LSR A \ Set (JJ II) = (A 0) >> 3 ROR II \ = A * 256 / 8 LSR A \ = A * 32 ROR II \ = arctanY(|x-delta| / |z-delta|) * 32 LSR A ROR II STA JJ LDA VV \ If VV and GG have different signs, then so do x-delta EOR GG \ and z-delta, so jump to rotn15 BPL rotn15 LDA #0 \ Negate (JJ II) SEC \ SBC II \ starting with the low bytes STA II LDA #0 \ And then the high bytes SBC JJ STA JJ .rotn15 LDA #0 \ Set A = 0, to add to the high byte below BIT GG \ If z-delta is positive, jump to rotn16 to skip the BPL rotn16 \ following instruction \ If we get here then z-delta is negative LDA #&80 \ Set A = -128, to add to the high byte below .rotn16 CLC \ Set (JJ II) = (JJ II) + (A 0) ADC JJ \ STA JJ \ which is one of the following: \ \ (JJ II) = (JJ II) \ \ (JJ II) = (JJ II) - 128 * 256 \ \ depending on the sign of z-delta RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: GetObjPitchAngle \ Type: Subroutine \ Category: 3D objects \ Summary: Calculate an object's pitch angle \ Deep dive: Pitch and yaw angles \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The offset of the variable to use for the object's 3D \ coordinates \ \ * &F4 = yHelmetCoord \ \ * &FA = yCoord1 \ \ * &FD = yCoord2 \ \ Y The offset of the second variable to use: \ \ * 0 = yPlayerCoord \ \ * 6 = yRoadSignCoord \ \ (L K) The result from GetObjectDistance, which is called \ between GetObjYawAngle and GetObjPitchAngle \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ LL The pitch angle of the object \ \ A The pitch angle of the object (same as LL) \ \ scaleUp The scale up factor for the object \ \ scaleDown The scale down factor for the object \ \ C flag Is the object visible on-screen: \ \ * Clear if the object is on-screen \ \ * Set if it isn't on-screen \ \ N flag Set according to the y-coordinate, so a BPL following \ the call will branch if the y-coordinate is positive \ \ ------------------------------------------------------------------------------ \ \ Other entry points: \ \ GetObjPitchAngle-2 Use yPlayerCoord (Y = 0) \ \ ****************************************************************************** LDY #0 \ Use xPlayerCoord for the second variable when calling \ the routine via GetObjPitchAngle-2 .GetObjPitchAngle \ The vectors used in this routine are configured by the \ values of X and Y, but for the purposes of simplicity, \ the comments will assume the following: \ \ * X = &FD, yCoord2 \ \ * Y = 0, yPlayerCoord LDA ySegmentCoordILo,X \ Set (WW QQ) = yCoord2 - yPlayerCoord SEC \ SBC yPlayerCoordHi,Y \ starting with the low bytes STA QQ LDA ySegmentCoordIHi,X \ And then the high bytes SBC yPlayerCoordTop,Y STA WW \ Let's call this difference in y-coordinates y-delta, \ so: \ \ (WW QQ) = (A QQ) = y-delta BPL pang1 \ If (A QQ) is positive, jump to pang1 to skip the \ following LDA #0 \ Set (A QQ) = 0 - (WW QQ) SEC \ SBC QQ \ starting with the low bytes STA QQ LDA #0 \ And then the high bytes SBC WW \ So (A QQ) is now positive, in other words: \ \ (A QQ) = |y-delta| .pang1 LSR A \ Set (A QQ) = (A QQ) >> 3 ROR QQ \ = |y-delta| / 8 LSR A ROR QQ LSR A ROR QQ STA TT \ Set (TT QQ) = (A QQ) \ = |y-delta| / 8 \ We now compare the two 16-bit values in (A QQ) and \ (L K) CMP L \ If A < L, then (A QQ) < (L K), so jump to pang3 BCC pang3 BNE pang2 \ If A <> L, i.e. A > L, then (A QQ) > (L K), so jump \ to pang2 to return from the subroutine with the C flag \ set \ The high bytes are equal, so now we compare the low \ bytes LDA QQ \ If QQ < K, then (A QQ) < (L K), so jump to pang3 CMP K BCC pang3 .pang2 \ If we get here then (A QQ) >= (L K), so: \ \ |y-delta| / 8 >= (L K) SEC \ Set the C flag RTS \ Return from the subroutine .pang3 LDY #0 \ Set Y = 0, which we use to count the number of shifts \ in the following calculation LDA L \ Set (A K) = (L K) JMP pang5 \ Jump to pang5 .pang4 \ This part is called from below, if we want to scale \ the division ASL QQ \ Set (TT QQ) = (TT QQ) << 1 ROL TT INY \ Increment Y .pang5 \ If we get here, then: \ \ * (TT QQ) = |y-delta| / 8 \ \ * WW is the high byte of y-delta \ \ * (A K) = |x-delta| \ \ * |x-delta| > |y-delta| / 8 \ \ * Y = 0 \ \ We now do the following division so we can use \ trigonometry to calculate the pitch angle: \ \ (|y-delta| / 8) / |x-delta| \ \ To get started, we shift both 16-bit values to the \ left as far as possible, which we can do without \ affecting the result as we are going to divide the two \ values, so any mutual shifts will cancel each other \ out in the division \ \ We count the number of shifts we do in Y \ \ Once that's done, we can drop the low bytes and just \ divide the high bytes, which retains as much accuracy \ as possible while avoiding the need for full 16-bit \ division \ \ So we keep shifting left until we get a 1 in bit 7 of \ (A K), as that's the larger of the two values ASL K \ Set (A K) = (A K) << 1 ROL A BCC pang4 \ If we just shifted a 0 out of the high byte of (A K), \ then we can keep shifting, so loop back to rotn6 to \ keep shifting both values ROR A \ We just shifted a 1 out of bit 7 of A, so reverse the \ shift so A contains the correct high byte (we don't \ care about the low byte any more) \ So by this point, (A K) and (TT QQ) have both been \ scaled by the same number of shifts STA V \ Set V = A, the high byte of the scaled |x-delta|, \ which we know is at least 128 (as bit 7 is set) STY scaleDown \ Set scaleDown to the number of shifts in Y TAY \ Set scaleUp = 256 / (1 + (A - 128) / 128) LDA divideX-128,Y \ = 256 / (1 + (|x-delta| - 128) / 128) STA scaleUp \ \ We know that A contains the scaled-up |x-delta|, which \ ranges from 128 (when x-delta is small) to 256 (when \ x-delta is large), so scaleUp contains the reciprocal \ of this, i.e. 1/|x-delta|, scaled into the range 256 \ to 128 LDA QQ \ Set T = QQ, the low byte of the scaled |y-delta|, to STA T \ use for rounding the result in Divide8x8 LDA TT \ Set A = TT, the high byte of the scaled |y-delta| JSR Divide8x8 \ Set T = 256 * A / V \ = 256 * (|y-delta| / 8) / |x-delta| \ \ using the lower byte of the |y-delta| numerator for \ rounding LDA T \ If T >= 128, jump to pang8 to return from the CMP #128 \ subroutine with the C flag set BCS pang8 BIT WW \ If y-delta is positive, jump to pang6 to skip the BPL pang6 \ following and add 60 to T LDA #60 \ Set A = 60 - T SEC SBC T JMP pang7 \ Jump to pang7 .pang6 CLC \ Set A = T + 60 ADC #60 .pang7 SEC \ Set LL = A - playerPitchAngle SBC playerPitchAngle STA LL CLC \ Clear the C flag to indicate success .pang8 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: GetSectionAngles (Part 1 of 3) \ Type: Subroutine \ Category: Track geometry \ Summary: Get the yaw and pitch angles for the inner and outer track \ sections \ Deep dive: Data structures for the track calculations \ The track verges \ \ ------------------------------------------------------------------------------ \ \ This routine does the following: \ \ * Part 1: If we have fetched a new track section since the last call, \ shuffle the track section list along by one so we can insert the new \ section \ \ * Part 1: Go through the track section list and apply spin to each valid \ entry (for both the right and left track section), skipping the entry \ pointed to by the sectionListPointer \ \ * Update the entry at sectionListPointer as follows: \ \ * Part 2: Calculate the track section number for this entry, relative to \ the front segment in the track segment buffer \ \ * Part 3: Store the yaw and pitch angles for this section in the \ xVergeRight/Left and yVergeRight/Left tables \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ xVergeRight Updated yaw angles for the entries in the track section \ list (i.e. indexes 0 to 5) for the right verge \ \ xVergeLeft Updated yaw angles for the entries in the track section \ list (i.e. indexes 0 to 5) for the left verge \ \ yVergeRight Updated pitch angles for the entries in the track \ section list (i.e. indexes 0 to 5) for the right verge \ \ yVergeLeft Updated pitch angles for the entries in the track \ section list (i.e. indexes 0 to 5) for the left verge \ \ horizonLine Updated to cater for the pitch angles of the updated \ track sections \ \ horizonListIndex Updated to the index of the track section that contains \ the horizon (i.e. the index within the track section \ list) \ \ ------------------------------------------------------------------------------ \ \ Other entry points: \ \ GetSectionAngles-1 Contains an RTS \ \ ****************************************************************************** .GetSectionAngles LDA newSectionFetched \ If newSectionFetched = 0, then we have not fetched a BEQ gsec1 \ new track section since the last call, so jump to \ gsec1 to skip the following call to ShuffleSectionList JSR ShuffleSectionList \ Shuffle the track section list along by one so we can \ insert the new section, updating sectionListValid and \ sectionListPointer accordingly LDA #0 \ Reset newSectionFetched to 0 so we don't call the STA newSectionFetched \ ShuffleSectionList routine again until the next new \ section has been fetched .gsec1 LDY sectionListStart \ If sectionListStart = 6, then the track section list CPY #6 \ is zero-length, so return from the subroutine (as BEQ GetSectionAngles-1 \ GetSectionAngles-1 contains an RTS) \ \ This never happens with the Silverstone track, as for \ this track, sectionListStart is in the range 2 to 5 \ (as sectionListSize is in the range 1 to 4) LDY sectionListValid \ If sectionListValid = 6 then there are no valid CPY #6 \ entries in the track section list, so jump to gsec4 to BEQ gsec4 \ skip the spinning process (as we only apply spin to \ valid sections in the list) \ Otherwise we now loop from Y = sectionListValid up to \ 5 to work through the valid entries in the list, \ applying the yaw angle spin to each one, and skipping \ entry number sectionListPointer as we are going to \ update that entry below .gsec2 CPY sectionListPointer \ If Y = sectionListPointer, jump to gsec3 to move on to BEQ gsec3 \ the next entry in the list, as we are going to update \ this entry below STY T \ Store Y in T so we can retrieve it below when applying \ spin to the left verge TYA \ Set Y = Y + 40 CLC \ ADC #40 \ So Y now points to the section for the right verge TAY JSR SpinTrackSection \ Apply the car's current spin to the right verge track \ section in Y: \ \ * Reset vergeDataRight to zero \ \ * Subtract spinYawAngle from the yaw angles in \ xVergeRightLo, xVergeRightHi \ \ * Subtract spinPitchAngle from the pitch angle in \ yVergeRight \ \ * Update horizonListIndex and horizonLine LDY T \ Retrieve the original value of Y that we stored above JSR SpinTrackSection \ Apply the car's current spin to the left verge track \ section in T: \ \ * Reset vergeDataLeft to zero \ \ * Subtract spinYawAngle from the yaw angles in \ xVergeLeftLo, xVergeLeftHi \ \ * Subtract spinPitchAngle from the pitch angle in \ yVergeLeft \ \ * Update horizonListIndex and horizonLine .gsec3 INY \ Increment the loop counter in Y CPY #6 \ Loop back until we have updated all the valid entries BCC gsec2 \ in the track section list \ ****************************************************************************** \ \ Name: GetSectionAngles (Part 2 of 3) \ Type: Subroutine \ Category: Track geometry \ Summary: Calculate the track section number for this track section entry \ Deep dive: Data structures for the track calculations \ The track verges \ \ ------------------------------------------------------------------------------ \ \ This part of the routine calculates the number of the track section that we \ want to update, i.e. the section at entry sectionListPointer in the list. \ \ ****************************************************************************** .gsec4 LDA #6 \ Set A = (6 - sectionListPointer) * 8 SEC \ SBC sectionListPointer \ This calculates the following: ASL A \ ASL A \ * A = 1 * 8 for entry #5 ASL A \ * A = 2 * 8 for entry #4 \ * A = 3 * 8 for entry #3 \ * A = 4 * 8 for entry #2 \ * A = 5 * 8 for entry #1 \ * A = 6 * 8 for entry #0 BIT directionFacing \ If bit 7 of directionFacing is clear, then we are BPL gsec5 \ facing forwards, so jump to gsec5 \ If we get here then we are facing backwards STA T \ Set T = A LDA objTrackSection+23 \ Set Y to the number * 8 of the track section for the \ front segment of the track segment buffer CLC \ Set A = A + 8 - T ADC #8 \ = frontSection * 8 + 8 - T SEC \ = (frontSection + 1 - (T / 8)) * 8 SBC T \ \ So A contains: \ \ * (frontSection - 0) * 8 for entry #5 \ * (frontSection - 1) * 8 for entry #4 \ * (frontSection - 2) * 8 for entry #3 \ * (frontSection - 3) * 8 for entry #2 \ * (frontSection - 4) * 8 for entry #1 \ * (frontSection - 5) * 8 for entry #0 \ \ So A now contains the correct section number for \ entry number sectionListPointer BCS gsec6 \ If the subtraction didn't underflow, jump to gsec6 ADC trackSectionCount \ The subtraction underflowed, so add the total number \ of track sections * 8 given in trackSectionCount to \ wrap round to the correct section number (we know the \ C flag is clear as we just passed through a BCS) JMP gsec6 \ Jump to gsec6 .gsec5 \ If we get here then we are facing forwards CLC \ Set A = A + number * 8 of track section for the ADC objTrackSection+23 \ front segment \ = A + frontSection * 8 \ \ So A contains: \ \ * (1 + frontSection) * 8 for entry #5 \ * (2 + frontSection) * 8 for entry #4 \ * (3 + frontSection) * 8 for entry #3 \ * (4 + frontSection) * 8 for entry #2 \ * (5 + frontSection) * 8 for entry #1 \ * (6 + frontSection) * 8 for entry #0 \ \ So A now contains the correct section number for \ entry number sectionListPointer CMP trackSectionCount \ If A < trackSectionCount then A is a valid section BCC gsec6 \ number, so jump to gsec6 SBC trackSectionCount \ The addition took us past the highest track section \ number, so subtract the total number of track sections \ * 8 given in trackSectionCount to bring it down to the \ correct section number (we know the C flag is set as \ we just passed through a BCC) \ ****************************************************************************** \ \ Name: GetSectionAngles (Part 3 of 3) \ Type: Subroutine \ Category: Track geometry \ Summary: Calculate the yaw and pitch angles for the track section entry \ that we want to update \ Deep dive: Data structures for the track calculations \ The track verges \ \ ------------------------------------------------------------------------------ \ \ This part of the routine sets the yaw and pitch angles for this track section \ in the xVergeRight/Left and yVergeRight/Left tables. \ \ ****************************************************************************** .gsec6 TAY \ Set Y = the section number * 8 that we calculated in \ part 3 STY thisSectionNumber \ Store the section number * 8 in thisSectionNumber, so \ we can retrieve it below when looping back LDX sectionListPointer \ Set X = sectionListPointer, to use as a counter in the \ two loops below \ We run the following section twice, once for the inner \ track section coordinates with X = sectionListPointer, \ and a second time for the outer track section \ coordinates with X = sectionListPointer + 40 .gsec7 STX sectionCounter \ Store the loop counter in sectionCounter LDX #&FD \ Copy the first trackSectionI coordinate for track JSR GetSectionCoord \ section Y into xCoord2, so xCoord2 is the 3D \ coordinate of the inner track at the start of the \ section (or, if this is the second loop where Y has \ been incremented by 3, xCoord2 is the 3D coordinate \ of the outer track) JSR GetObjYawAngle-2 \ Calculate xCoord2's yaw angle, from the point of view \ of the player, returning it in (JJ II) LDY sectionCounter \ Set Y to the loop counter BIT directionFacing \ If bit 7 of directionFacing is clear, then we are BPL gsec8 \ facing forwards, so jump to gsec8 TYA \ We are facing backwards, so flip Y between EOR #40 \ sectionListPointer and sectionListPointer + 40 to do TAY \ the inner and outer track sections in reverse order \ (so we always do the right track verge first, then the \ left track verge, where right and left are relative to \ the direction we are facing) .gsec8 JSR GetSectionYawAngle \ Set the following for the Y-th section, to calculate \ the difference in yaw angle between the track section \ and the player: \ \ xVergeRight = (JJ II) - playerYawAngle \ \ Also set (L K) to the distance between the track \ section and the player's car LDX sectionCounter \ If the loop counter in X >= 40, then we are dealing CPX #40 \ with the outer track section, so jump to gsec10 as we BCS gsec10 \ don't need to repeat the pitch angle calculation (the \ track is level from left to right, so the outer track \ is the same pitch angle as the inner track) LDX #&FD \ Set X = &FD so the call to GetObjPitchAngle uses \ xCoord2, which we set above to the 3D coordinate of \ the inner track at the start of the section JSR GetObjPitchAngle-2 \ Calculate xCoord2's pitch angle, from the point \ of view of the player, returning it in A and LL LDX sectionCounter \ Set X to the loop counter, which we know is less than \ 40 at this point (and which is therefore equal to \ sectionListPointer) LDA LL \ Set A to the pitch angle that we just calculated \ for the track section STA yVergeRight,X \ Store the pitch angle in the X-th yVergeRight \ entry, for this point on the right track section STA yVergeLeft,X \ Store the same pitch angle in the X-th yVergeLeft, \ for this point on the left track section, which will \ at the same pitch angle as the track is level from \ left to right CMP horizonLine \ If A < horizonLine, then this track section is lower BCC gsec10 \ than the current horizon, so jump to gsec10 to move on \ to the outer track section, as this section will not \ be obscuring the horizon BNE gsec9 \ If A <> horizonLine, i.e. A > horizonLine, then this \ means the track section is higher than the current \ horizon line, so jump to gsec9 to set the horizon \ line to the pitch angle of this track section, as the \ section is obscuring the horizon \ If we get here, then A = horizonLine, so this section \ is at the same pitch angle as the current horizon line CPX horizonListIndex \ If X < horizonListIndex, then this section has a lower BCC gsec10 \ index than the current horizon section, so jump to \ gsec10 as horizonListIndex already contains the higher \ index, and a higher index is closer to the player, so \ we don't need to change the horizon line details .gsec9 \ If we get here then we want to update the horizon to \ the pitch angle of the track section we are updating, \ as it obscures the horizon STA horizonLine \ Set horizonLine to the pitch angle in A, so the \ horizon is set to the pitch angle of this track \ section STX horizonListIndex \ Store the index of this section in the track section \ list in horizonListIndex .gsec10 TXA \ Set A = X + 40 CLC \ = sectionListPointer + 40 ADC #40 \ \ So A now points to the outer track section coordinates \ and is ready to be put into X (and, when we look back, \ into sectionCounter) for the loop back to gcsec7 below CMP #60 \ If A >= 60, we have done both inner and outer track BCS gsec11 \ sections, so jump to gsec11 TAX \ Set X = A \ = sectionListPointer + 40 LDA thisSectionNumber \ Set Y = thisSectionNumber + 3 CLC \ ADC #3 \ So when we loop back, the offset in Y points to the TAY \ trackSectionO coordinates for the outer track section \ instead of the inner coordinates in trackSectionI (as \ the outer coordinates are 3 bytes after the inner ones \ in the track data) JMP gsec7 \ Loop back to gsec7 to process the outer track section .gsec11 \ If we get here then we have updated this entry in the \ track section list with both left and right angles, so \ we now update the list pointers LDX sectionListPointer \ Set X = sectionListPointer - 1 DEX JSR SetSectionPointers \ Update the section list pointers to move down through \ the track section list LDA #7 \ If prevHorizonIndex <= 7, then the previous call to CMP prevHorizonIndex \ GetTrackAndMarkers (on the last iteration of the main BCS gsec12 \ driving loop) had the horizon on one of the sections \ in the track section list, or the first entry in the \ track segment list (as the list starts at index 6), so \ jump to gsec12 to skip the following STA horizonLine \ If we get here then the previous iteration around the \ main loop had the horizon line on one of the track \ segments in the track segment list (but not the first \ entry in the list), so set horizonLine to 7 .gsec12 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: GetSegmentYawAngle \ Type: Subroutine \ Category: Track geometry \ Summary: Calculate the difference in yaw angle between a track segment and \ the player \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The offset from xSegmentCoordILo of the segment's 3D \ coordinates, i.e. the segment number * 3, with: \ \ * X for inner track segment coordinates \ \ * X + 120 for outer track segment coordinates \ \ segmentListPointer The index of the segment in the track segment list to \ use for calculations \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ (L K) The distance between the object and the player's car \ \ A Contains the high byte of (L K) \ \ ****************************************************************************** .GetSegmentYawAngle JSR GetObjYawAngle-2 \ Calculate the segment's yaw angle, from the point of \ view of the player, returning it in (JJ II) LDY segmentListPointer \ Set Y = segmentListPointer, so the result gets stored \ in the correct position in the track segment list \ Fall through into GetSectionYawAngle to set the \ specified xVergeRight or xVergeLeft to the difference \ in the yaw angle between the player and the segment \ ****************************************************************************** \ \ Name: GetSectionYawAngle \ Type: Subroutine \ Category: Track geometry \ Summary: Calculate the difference in yaw angle between an object and the \ player \ \ ------------------------------------------------------------------------------ \ \ This routine is typically used to calculate the difference in yaw angle \ between a track section and the player. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ Y Index from xVergeRight to store the difference in yaw \ angle between the object and the player \ \ (JJ II) The yaw angle of the object \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ xVergeRight The difference in the yaw angle between the object and \ the player (if Y points to the right verge) \ \ xVergeLeft The difference in the yaw angle between the object and \ the player (if Y points to the left verge) \ \ (L K) The distance between the object and the player's car \ \ A Contains the high byte of (L K) \ \ M The smaller yaw angle of the object, where 0 to 255 \ represents 0 to 45 degrees \ \ ****************************************************************************** .GetSectionYawAngle LDA II \ Set the following for the Y-th section: SEC \ SBC playerYawAngleLo \ xVergeRight = (JJ II) - playerYawAngle STA xVergeRightLo,Y \ \ starting with the low bytes LDA JJ \ And then the high bytes SBC playerYawAngleHi STA xVergeRightHi,Y JMP GetObjectDistance \ Set (L K) to the distance between the object and the \ player's car, with A set to L, returning from the \ subroutine using a tail call \ ****************************************************************************** \ \ Name: GetSegmentAngles (Part 1 of 3) \ Type: Subroutine \ Category: Track geometry \ Summary: Get the yaw and pitch angles for the inner or outer track segments \ Deep dive: Data structures for the track calculations \ The track verges \ \ ------------------------------------------------------------------------------ \ \ This routine works through track segments, starting from distant segments and \ working backwards towards the player, calculating the angles and verge data \ for each segment as we go, up to a maximum of 16 segments (which is the \ capacity of the track segment list). \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ A The index of the first segment to update in the track \ segment list, starting at 6 for the first entry in list \ of right segments, and 46 for the first entry in the \ list of left segments \ \ X The offset from xSegmentCoordILo of the segment's 3D \ coordinates, i.e. the segment number * 3, with: \ \ * X for inner track segment coordinates \ \ * X + 120 for outer track segment coordinates \ \ segmentOffset The offset to use for this segment: \ \ * 0 when our car is facing in the same direction \ \ * 120 when our car is facing the opposite direction \ \ segmentDirection The relative direction of our car: \ \ * 0 when our car is facing in the same direction \ \ * 1 when our car is facing the opposite direction \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ xVergeRight Updated yaw angles for the entries in the track segment \ list (i.e. indexes 6 to 21) for the right verge \ \ yVergeLeft Updated pitch angles for the entries in the track \ segment list (i.e. indexes 6 to 21) for the left verge \ \ edgeDistance The distance between the player's car and the nearest \ track edge \ \ edgeSegmentNumber The number of the segment within the track segment list \ that is closest to the player's car \ \ edgeSegmentPointer The index of the segment within track verge buffer that \ is closest to the player's car \ \ edgeYawAngle The yaw angle of the segment that is closest to the \ player's car \ \ xVergeRight Entries in the second part of the track segment list for \ the coordinates of the outside of the right track verge \ (i.e. indexes 22 to 37, which correspond to the yaw \ angles in the track segment list in indexes 6 to 21) \ \ xVergeLeft Entries in the second part of the track segment list for \ the coordinates of the outside of the left track verge \ (i.e. indexes 22 to 37, which correspond to the yaw \ angles in the track segment list in indexes 6 to 21) \ \ yVergeRight Pitch angles for the entries in the track segment \ list (i.e. indexes 6 to 21) for the right verge \ \ yVergeLeft Pitch angles for the entries in the track segment \ list (i.e. indexes 6 to 21) for the left verge \ \ xMarker Distance in the x-axis between the track edge and the \ corner marker for this segment (if there is one) \ \ vergeDataRight Data (such as colour) for this segment's right verge \ \ vergeDataLeft Data (such as colour) for this segment's left verge \ \ ****************************************************************************** .GetSegmentAngles STA segmentListPointer \ Set segmentListPointer to the index passed in A LDA #0 \ Set segmentCounter = 0, to use to count visible STA segmentCounter \ segments over the course of the following routine \ We now run the rest of the routine for each segment \ in turn, looping back to here while segments are \ visible .gseg1 JSR GetSegmentYawAngle \ Calculate the yaw angle and distance between the \ player's car and the track segment specified in X, and \ store the results in the track segment list at the \ segment list pointer \ \ Also set (A K) = (L K) = the distance between the car \ and the track segment \ We now check to see if this is the closest track \ segment we've come across in this iteration of the \ main loop, and if it is, we set a bunch of variables \ with the details of the track edge CMP edgeDistanceHi \ If A < edgeDistanceHi, then we know that (A K) and BCC gseg2 \ therefore (L K) < (edgeDistanceHi edgeDistanceLo), \ so jump to gseg2 to set (L K) as the new minimum \ distance to the verge BNE gseg3 \ If A <> edgeDistanceHi, i.e. A > edgeDistanceHi, \ then (L K) > (edgeDistanceHi edgeDistanceLo), so \ jump to gseg3 as (L K) is not a new minimum verge \ distance \ We now compare the high bytes LDA edgeDistanceLo \ If edgeDistanceLo < K, then we know that CMP K \ (L K) > (edgeDistanceHi edgeDistanceLo), so jump to BCC gseg3 \ gseg3 as (L K) is not a new minimum verge distance .gseg2 \ If we get here then we know that \ (L K) <= (edgeDistanceHi edgeDistanceLo), so we now \ set (L K) as the new minimum distance to the verge, \ and set a number of variables so we can refer to this \ nearest track edge in places like the crash routine LDA L \ Set (edgeDistanceHi edgeDistanceLo) = (L K) STA edgeDistanceHi LDA K STA edgeDistanceLo LDA segmentCounter \ Set edgeSegmentNumber = segmentCounter STA edgeSegmentNumber \ \ So edgeSegmentNumber contains the number of the \ segment within the track segment list that is closest \ to the player's car LDY segmentListPointer \ Set edgeSegmentPointer = segmentListPointer STY edgeSegmentPointer \ \ So edgeSegmentPointer contains the index of the \ segment within the track verge buffer (i.e. from \ xVergeRight) that is closest to the player's car LDA xVergeRightHi,Y \ Set edgeYawAngle = the segment's entry in STA edgeYawAngle \ xVergeRightHi \ \ So edgeYawAngle contains the yaw angle of the segment \ that is closest to the player's car, from the point of \ view of the car .gseg3 JSR GetObjPitchAngle-2 \ Calculate the segment's pitch angle, from the \ point of view of the player, returning it in A and LL \ \ If the segment is not visible on-screen, the C flag is \ set, otherwise it will be clear BCS gseg4 \ If the segment is not visible on-screen, jump to gseg4 BPL gseg10 \ If the pitch angle is positive, jump to gseg10 \ ****************************************************************************** \ \ Name: GetSegmentAngles (Part 2 of 3) \ Type: Subroutine \ Category: Track geometry \ Summary: Process a segment that is not visible by trying to process a \ segment that's one-quarter of the size \ Deep dive: Data structures for the track calculations \ The track verges \ \ ****************************************************************************** .gseg4 \ If we get here then the segment is not visible or the \ segment's pitch angle is negative, so we need to \ stop processing segments \ \ However, before we stop, we try to eke out as much \ accuracy out of the last (not visible) segment by \ trying to process a segment that's one-quarter of the \ size, just in case this smaller segment is visible, in \ which case we finish with something to show for the \ last visible segment (and if not, at least we tried) LDA segmentCounter \ If segmentCounter is non-zero then we have already BNE gseg5 \ found at least one visible segment, so jump to gseg5 RTS \ Otherwise this is the first segment and it's not \ visible, so return from the subroutine as none of the \ others will be either (as we are working backwards \ through the segments towards the player, so if the \ first segment is not visible, so will all the ones \ behind it) .gseg5 LDA #0 \ Set U = 0, to use as an axis counter below STA U LDY prevSegmentOffset \ Set Y to the offset from xSegmentCoordILo of the \ previous segment's 3D coordinates STX W \ Store the segment offset that's in X in W, so we can \ retrieve it below \ We now loop through all three axes, calculating the \ difference in 3D coordinates between this segment and \ the previous segment for xSegmentCoord, ySegmentCoord \ and zSegmentCoord .gseg6 LDA xSegmentCoordILo,X \ Set (A T) to the difference between the coordinate SEC \ of the previous segment and this one, starting with SBC xSegmentCoordILo,Y \ the low bytes STA T LDA xSegmentCoordIHi,X \ And then the high bytes SBC xSegmentCoordIHi,Y CLC \ Clear the C flag BPL gseg7 \ If the result in (A T) is positive, then jump to gseg7 SEC \ Set the C flag, to indicate that the result is \ negative .gseg7 PHP \ Store the C flag on the stack, which contains the sign \ bit of the result (0 for positive, 1 for negative), so \ we can use it to rotate the correct sign but into \ (A T) in the following ROR A \ Set (A T) = (A T) >> 1 ROR T \ \ making sure to retain the correct sign in bit 7 PLP \ Fetch the C flag from the stack, so it once again \ contains the correct sign for (A T) ROR A \ Set (A T) = (A T) >> 1 ROR T \ \ making sure to retain the correct sign in bit 7 STA V \ Set (V T) = (A T) \ \ So (V T) contains the difference in 3D coordinates, \ divided by 4, and with the correct sign retained LDX U \ Set X to the axis counter in U \ \ For clarity, the following comments will assume we are \ working with the x-axis LDA xSegmentCoordILo,Y \ Set xCoord1 = xSegmentCoord for previous segment CLC \ + (V T) ADC T \ STA xCoord1Lo,X \ starting with the low bytes LDA xSegmentCoordIHi,Y \ And then the high bytes ADC V STA xCoord1Hi,X INX \ Increment the axis counter in X CPX #3 \ If X = 3, we have done all three axes, so jump to BEQ gseg8 \ gseg8 STX U \ Store the incremented value in U, so this is the same \ as incrementing U LDX W \ Set X = W, which we set above to the segment offset \ for the current segment INY \ Increment Y to point to the next axis for the previous \ segment INX \ Increment X to point to the next axis for the current \ segment STX W \ Store the updated segment offset for the current \ segment in W JMP gseg6 \ Loop back to gseg6 to process the next axis .gseg8 \ By the time we get here, xCoord1 contains the 3D \ coordinates of the previous segment, plus a quarter \ of the vector from the previous segment to the current \ segment LDX #&FA \ Set X = &FA so the call to GetSegmentYawAngle uses \ xCoord1 JSR GetSegmentYawAngle \ Calculate the yaw angle and distance between the \ player's car and xCoord1, and store the results in \ the track segment list at the segment list pointer \ \ Also set (A K) = (L K) = the distance between the car \ and xCoord1 JSR GetObjPitchAngle-2 \ Calculate xCoord1's pitch angle, from the point \ of view of the player, returning it in A and LL \ \ If xCoord1 is not visible on-screen, the C flag is \ set, otherwise it will be clear BCS gseg9 \ If xCoord1 is not visible on-screen, jump to gseg9 \ to return from the subroutine \ If we get here then xCoord1 is visible, so we can \ store the results as our final entry in the track \ segment list LDX prevSegmentOffset \ Set X to the offset from xSegmentCoordILo of the \ previous segment's 3D coordinates, so the call to \ GetVergeAndMarkers uses the previous segment's verge \ data for our quarter segment's calculation LDA markersToDraw \ Store markersToDraw in markerNumber so we can restore STA markerNumber \ it after the call to GetVergeAndMarkers (so the call \ doesn't change the value of markersToDraw, as we \ don't want to try drawing markers with this \ quarter-size segment) JSR GetVergeAndMarkers \ Get the details for the previous segment's corner \ markers and verge marks and store them for this \ segment LDA markerNumber \ Retrieve the value of markersToDraw that we stored STA markersToDraw \ in markerNumber, so any marker calculations in the \ above call get ignored INC segmentListPointer \ Increment the segment list pointer as we just added a \ new entry to the track segment list .gseg9 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: GetSegmentAngles (Part 3 of 3) \ Type: Subroutine \ Category: Track geometry \ Summary: Process a visible segment \ Deep dive: Data structures for the track calculations \ The track verges \ \ ****************************************************************************** .gseg10 \ If we get here then the segment's pitch angle is \ positive and the segment is visible on-screen JSR GetVergeAndMarkers \ Get the details for this segment's corner markers and \ verge marks LDA segmentCounter \ If segmentCounter <= edgeSegmentNumber, jump to gseg13 CMP edgeSegmentNumber \ to keep checking segments, as we haven't yet gone past BEQ gseg13 \ the closest segment to the player BCC gseg13 \ If we get here then we have gone past the closest \ segment to the player, so we need to check whether the \ segment is within the 20-degree field of view, and \ stop when the segments become hidden from view LDY segmentListPointer \ Set Y to the segment list pointer LDA xVergeRightHi,Y \ Set A to the high byte of the yaw angle of the \ segment's right verge BPL gseg11 \ If the angle is negative, negate it, so we now have EOR #&FF \ A = |yaw angle| .gseg11 CMP #20 \ If A < 20, then the segment is within the 20-degree BCC gseg13 \ field of view, so jump to gseg13 to keep checking \ segments LDA xVergeRightHi-1,Y \ Set A to the high byte of the yaw angle of the \ previous segment's right verge BPL gseg12 \ If the angle is negative, negate it, so we now have EOR #&FF \ A = |yaw angle| .gseg12 CMP #20 \ If A >= 20, then the previous segment was also outside BCS gseg16 \ the 20-degree field of view, so jump to gseg16 to \ return from the subroutine JMP gseg4 \ If we get here then the current segment is outside the \ 20-degree field of view, but the previous one wasn't, \ so we jump to gseg4 to try processing a segment that's \ one-quarter of the size, in case that fits .gseg13 \ If we get here then we have successfully processed a \ visible segment STX prevSegmentOffset \ Store the offset from xSegmentCoordILo of this \ segment's 3D coordinates in prevSegmentOffset, to use \ in the next iteration if the next segment is not \ visible INC segmentListPointer \ Increment the segment list pointer to point to the \ next entry in the list INC segmentCounter \ Increment the segment counter to indicate that we have \ populated a visible segment (so this will become 1 if \ this is the first visible segment we have processed, \ 2 for the second visible segment, and so on) LDY segmentCounter \ Set Y to the number of visible segments we have \ populated so far CPY #18 \ If Y >= 18, i.e. Y > 17, then Y was > 16 before we BCS gseg16 \ incremented it, which means we have filled the track \ segment list with 16 visible segments, so we jump to \ gseg16 to return from the subroutine to stop \ processing segments LDA segmentStep,Y \ Set T to the segment step for segment number Y, so to STA T \ get the next segment, we step back T steps in the \ track segment buffer \ \ This makes us step a long way backwards for the first \ few segments, and then make shorter steps as we get \ closer to the player TXA \ Set A to the segment offset that's in X SEC \ Set A = A - segmentOffset SBC segmentOffset \ \ so A contains the number of the segment * 3 (as X \ contains the offset from xSegmentCoordILo, which will \ be 120 + X for the X-th outer segment coordinate, so \ subtracting segmentOffset brings the offset down to \ the number * 3, whether this is an inner or outer \ coordinate) CMP T \ If A >= T, jump to gseg14 to jump back T segments BCS gseg14 \ towards the player, for the next iteration \ If we get here then A < T, so we can't jump back T \ segments or we will jump past the beginning of the \ track segment buffer, so we need to add 120 to wrap \ around to the end of the buffer before we can jump \ back T segments TXA \ Set A to the segment offset that's in X CLC \ Set A = A + 120 to wrap around to the end of the track ADC #120 \ segment buffer JMP gseg15 \ Jump to gseg15 .gseg14 TXA \ Set A to the segment offset that's in X .gseg15 SEC \ Set X = A - T SBC T \ TAX \ So the next segment to be tested is T steps backwards \ in the track segment buffer, towards the player (so \ if T = 3, we step back one segment, or if T = 13 * 3, \ we step back 13 segments) JMP gseg1 \ Loop back to gseg1 to move on to the next segment .gseg16 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: MovePlayerSegment \ Type: Subroutine \ Category: Car geometry \ Summary: Move the player's car in the correct direction \ Deep dive: Placing cars on the track \ \ ------------------------------------------------------------------------------ \ \ This routine checks whether the player has turned enough to be in a different \ direction (i.e. pointing forwards to pointing backwards or vice versa), and if \ so, it turns the player around by updating the track segment buffer for the \ new direction, resetting the track section list, and updating all the \ direction-related variables. \ \ Otherwise it works out whether the player has moved into a new segment, and if \ so, it updates the car's segment and section numbers accordingly. \ \ ****************************************************************************** .MovePlayerSegment LDA playerHeading \ Set A = playerHeading - spinYawAngleTop SEC \ SBC spinYawAngleTop \ So A contains the new heading of the player's car, \ once the current spin is added (i.e. it's the new \ heading of the car) \ A is an angle that represents the new direction in \ which our car will be facing, after applying spin, \ with respect to the track, like this: \ \ 0 \ -32 | +32 Overhead view of car \ \ | / \ \ | / 0 = looking straight ahead \ \|/ +64 = looking sharp right \ -64 -----+----- +64 -64 = looking sharp left \ /|\ \ / | \ \ / | \ \ -96 | +96 \ 128 \ \ An angle of 0 means our car is facing forwards along \ the track, while an angle of +32 means we are facing \ 45 degrees to the right of straight on, and an angle \ of 128 means we are facing backwards along the track BPL mpla1 \ If A is positive, jump to mpla1 to skip the following EOR #&FF \ Invert A, so this effectively reflects the angle into \ the right half of the above diagram: \ \ 0 \ | 32 \ | / \ | / \ |/ \ +----- 64 \ |\ \ | \ \ | \ \ | 96 \ 127 .mpla1 ASL A \ Set A = A << 1, so we now have: \ \ 0 \ | 64 \ | / \ | / \ |/ \ +----- 128 \ |\ \ | \ \ | \ \ | 192 \ 254 CMP #128 \ Clear the C flag if A < 128 (i.e. top-right quadrant) \ is set the C flag if A >= 128 (i.e. bottom-right \ quadrant) \ Note that bit 7 is similar, so we have: \ \ 0 \ | 64 \ | / <-- C flag and bit 7 clear \ | / \ |/ \ +----- 128 \ |\ \ | \ \ | \ <-- C flag and bit 7 set \ | 192 \ 254 EOR directionFacing \ If we are facing forwards, leave A alone, but if we \ are currently facing backwards, flip bit 7 of A BPL mpla3 \ If we are facing forwards and we are in the top-right \ quadrant, or we are facing backwards and we are in the \ bottom-right quadrant, then the direction we are \ facing is still correct, so jump to mpla3 to get on \ with moving the car \ \ Otherwise we may now be facing in a different \ direction to before, and bit 7 of A is set BCC mpla2 \ If bit 7 of A was clear before the above EOR, then we \ are in the top-right quadrant but are currently facing \ backwards, so jump to mpla2 to skip the following \ instruction EOR #%01111111 \ Bit 7 of A was set before the above EOR, so we are in \ the bottom-right quadrant, but are currently facing \ forwards, so flip bits 0-6 of A, changing the range of \ the bottom-right quadrant from 128 to 254 to \ 255 to 129 .mpla2 \ By this point, we are pointing in the opposite \ direction to the setting of directionFacing, and the \ angles are as follows: \ \ 0 \ | 64 \ | / <-- C flag and bit 7 clear \ | / \ |/_.- 127 \ +----- 255 \ |\-._ 252 \ | \ \ | \ <-- C flag and bit 7 set \ | 192 \ 129 \ \ So 0 to 127 is in the top-right quadrant, while 255 to \ 129 is the bottom-right quadrant CMP #252 \ If A >= 252, then the new angle we are facing is in BCS mpla3 \ the top sliver of the bottom-right quadrant, so jump \ to mpla3 to get on with moving the car \ If we get here then A < 252, which means we are either \ now in the top-right quadrant, or we are in the bottom \ part of the bottom-right quadrant, and we are facing \ in a different direction to directionFacing \ \ So we have now officially turned in the opposite \ direction, and need to update all the various buffers \ and variables JSR ChangeDirection \ Turn the player around by updating the track segment \ buffer for the new direction, resetting the track \ section list, and updating all the direction-related \ variables RTS \ Return from the subroutine .mpla3 \ The GetSegmentAngles routine, which has already been \ called by this point, sets up the track segment list \ and sets edgeSegmentNumber to the entry number within \ the track segment list that is closest to the player's \ car \ \ Entry 13 in the track segment list corresponds to the \ segment that's 32 behind the front segment of the \ track segment buffer, which is the position of the \ player's car, so if edgeSegmentNumber does not equal \ 13, then it means that the car has moved into a new \ segment \ \ Specifically, the values of edgeSegmentNumber mean \ the following: \ \ * 11 = player has moved forward two segments \ * 12 = player has moved forward one segment \ * 13 = player is still in the same segment \ * 14 = player has moved back one segment \ * 15 = player has moved back two segments \ \ The player can't travel more than two segments in one \ iteration of the main driving loop LDA edgeSegmentNumber \ If edgeSegmentNumber = 12, jump to mpla4 to move the CMP #12 \ player forward by one segment BEQ mpla4 BCS mpla5 \ If edgeSegmentNumber > 12, then the player is either \ in the same segment, or has moved backwards, so jump \ to mpla5 \ If we get here then edgeSegmentNumber < 12, so \ edgeSegmentNumber must be 11, so we move the player \ forwards by two segments JSR MovePlayerForward \ Move the player forwards by one segment .mpla4 BIT playerPastSegment \ If bit 0 of playerPastSegment is clear, then the BPL mpla7 \ player has not yet gone past the closest segment, so \ jump to mpla7 to return from the subroutine without \ moving forward by this segment JSR MovePlayerForward \ Move the player forwards by one segment RTS \ Return from the subroutine .mpla5 \ If we get here then edgeSegmentNumber > 12 CMP #14 \ If edgeSegmentNumber < 14, i.e. edgeSegmentNumber is BCC mpla7 \ 13, then the player has not changed segment, so jump \ to mpla7 to return from the subroutine BEQ mpla6 \ If edgeSegmentNumber = 14, jump to mpla6 to move the \ player backwards by one segment \ If we get here then edgeSegmentNumber > 14, so \ edgeSegmentNumber must be 15, so we move the player \ backward by two segments JSR MovePlayerBack \ Move the player backwards by one segment .mpla6 JSR MovePlayerBack \ Move the player backwards by one segment .mpla7 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: GetTrackAndMarkers \ Type: Subroutine \ Category: Track geometry \ Summary: Calculate the 3D coordinates of the track and corner markers \ Deep dive: The track verges \ Corner markers \ \ ------------------------------------------------------------------------------ \ \ Other entry points: \ \ gtrm2+6 Calculate the following for segment Y: \ \ horizonTrackWidth = |xVergeRightHi - xVergeLeftHi| / 2 \ \ ****************************************************************************** .GetTrackAndMarkers LDA #0 \ Set horizonLine = 0, so we can calculate a new pitch STA horizonLine \ angle for the horizon in the following process JSR GetSectionAngles \ Get the yaw and pitch angles for the inner and outer \ track sections in the track section list and store the \ results in xVergeRight/Left and yVergeRight/Left LDA #255 \ Set edgeDistanceHi = 255, so GetSegmentAngles can set STA edgeDistanceHi \ it to the distance of the nearest verge LDA #13 \ Set edgeSegmentNumber = 13, as the default value for STA edgeSegmentNumber \ the number of the segment within the track segment \ list that is closest to the player's car LDA #0 \ Fetch the index details of the right track segments JSR GetSegmentDetails LDA #6 \ Get the yaw and pitch angles for the segments (and the JSR GetSegmentAngles \ verge marks and corner markers) along the right side \ of the track and store the results in xVergeRight, \ yVergeRight, xMarker and vergeDataRight LDA segmentListPointer \ Set segmentListRight = segmentListPointer STA segmentListRight \ \ So it contains the index of the last entry in the \ track segment list for the right side of the track LDA #%10000000 \ Fetch the index details of the left track segments JSR GetSegmentDetails LDA #46 \ Get the yaw and pitch angles for the segments (and the JSR GetSegmentAngles \ verge marks and corner markers) along the left side \ of the track and store the results in xVergeLeft, \ yVergeLeft, xMarker and vergeDataLeft LDA horizonListIndex \ If horizonListIndex < 40, then this is a valid index CMP #40 \ into the track verge buffer so jump to gtrm1 to skip BCC gtrm1 \ the following three instructions SEC \ Set horizonListIndex = horizonListIndex - 40 SBC #40 \ STA horizonListIndex \ so if we set horizonListIndex to the index for the \ outer track coordinates, this corrects the value to \ the index for the inner coordinates .gtrm1 TAY \ Set Y to the corrected value of horizonListIndex STY prevHorizonIndex \ Store the horizon section index in prevHorizonIndex, \ so we can refer to it in the next call to \ GetTrackAndMarkers LDA horizonLine \ If horizonLine < 79, then the horizon line is a valid CMP #79 \ number, so jump to gtrm2 to skip the following two BCC gtrm2 \ instructions LDA #78 \ Set horizonLine = 78, so the maximum value for the STA horizonLine \ horizon line is 78 .gtrm2 STA yVergeRight,Y \ Set the pitch angle for the right side of the horizon \ line in the track verge buffer to the updated value of \ horizonLine STA yVergeLeft,Y \ Set the pitch angle for the left side of the horizon \ line in the track verge buffer to the updated value of \ horizonLine LDA xVergeRightHi,Y \ Set A = xVergeRightHi - xVergeLeftHi for the horizon SEC \ section SBC xVergeLeftHi,Y JSR Absolute8Bit \ Set A = |A|, so A contains the arc of the track at \ the horizon (i.e. the track width on the section or \ segment at the horizon) in terms of the high bytes LSR A \ Set horizonTrackWidth = |A| / 2 STA horizonTrackWidth \ \ So horizonTrackWidth contains half the width of the \ track on the horizon, in terms of the high bytes RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: GetSegmentDetails \ Type: Subroutine \ Category: Track geometry \ Summary: Get the details for the segment in front or behind \ Deep dive: Data structures for the track calculations \ The track verges \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ A The direction in which to fetch a segment: \ \ * Bit 7 clear = forwards (right) \ \ * Bit 7 set = backwards (left) \ \ In other words, fetch the track segments from the right \ or left verges, according to the way we are facing \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ segmentOffset The offset to use for this segment: \ \ * 0 when our car is facing in direction A \ \ * 120 when our car is facing opposite direction A \ \ segmentDirection The relative direction of our car: \ \ * 0 when our car is facing in direction A \ \ * 1 when our car is facing opposite direction A \ \ X Returns: \ \ * frontSegmentIndex when our car is facing in \ direction A \ \ * frontSegmentIndex + 120 when our car is facing the \ opposite direction to A (so we use the outer \ xSegmentCoordOLo rather than the inner \ xSegmentCoordILo) \ \ ****************************************************************************** .GetSegmentDetails LDX frontSegmentIndex \ Set X to the index * 3 of the front track segment in \ the track segment buffer EOR directionFacing \ If bit 7 of A and bit 7 of directionFacing are the BPL segd1 \ same, jump to segd1 TXA \ Set X = X + 120 CLC ADC #120 TAX LDA #120 \ Set A = 120, so segmentOffset gets set to 120 SEC \ Set the C flag, so segmentDirection gets set to 1 BNE segd2 \ Jump to segd2 (this BNE is effectively a JMP as A is \ never zero) .segd1 LDA #0 \ Set A = 0, so segmentOffset gets set to 0 CLC \ Clear the C flag, so segmentDirection gets set to 0 .segd2 STA segmentOffset \ Set segmentOffset = A LDA #0 \ Set segmentDirection to the C flag ROL A STA segmentDirection RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: GetVergeAndMarkers (Part 1 of 4) \ Type: Subroutine \ Category: Track geometry \ Summary: Get the details for a segment's corner markers and verge marks \ Deep dive: The track verges \ Corner markers \ \ ------------------------------------------------------------------------------ \ \ The track verge, which is shown in black-and-white or red-and-white verge \ marks according to the track data, extends outwards from the track edge, where \ the track edge is the line defined by the segment vectors. \ \ This routine calculates the verge colours and the coordinates of the outside \ of the verge, and it also calculates the coordinates and colours of the corner \ markers for this track segment. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The offset from xSegmentCoordILo of the segment's 3D \ coordinates, i.e. the segment number * 3, with: \ \ * X for inner track segment coordinates \ \ * X + 120 for outer track segment coordinates \ \ LL The segment's pitch angle, from the point of view of \ the player \ \ scaleUp The scale up factor for the segment \ \ scaleDown The scale down factor for the segment \ \ Results: \ \ xVergeRight Entries in the second part of the track segment list for \ the coordinates of the outside of the right track verge \ (i.e. indexes 22 to 37, which correspond to the yaw \ angles in the track segment list in indexes 6 to 21) \ \ xVergeLeft Entries in the second part of the track segment list for \ the coordinates of the outside of the left track verge \ (i.e. indexes 22 to 37, which correspond to the yaw \ angles in the track segment list in indexes 6 to 21) \ \ yVergeRight Pitch angles for the entries in the track segment \ list (i.e. indexes 6 to 21) for the right verge \ \ yVergeLeft Pitch angles for the entries in the track segment \ list (i.e. indexes 6 to 21) for the left verge \ \ xMarker Distance in the x-axis between the track edge and the \ corner marker for this segment (if there is one) \ \ vergeDataRight Data (such as colour) for this segment's right verge \ \ vergeDataLeft Data (such as colour) for this segment's left verge \ \ ****************************************************************************** .GetVergeAndMarkers LDY segmentDirection \ Set Y to segmentDirection, which will be 0 when our \ car is facing in the same direction as the segment we \ are checking, or 1 if it's the opposite direction \ \ This determines whether we are creating the left or \ right verge, with 0 for the left verge and 1 for the \ right verge CPX #120 \ If X >= 120, jump to gmar1 to subtract 120 from the BCS gmar1 \ offset LDA segmentFlags,X \ Set A to the flags for this track segment from the \ track segment buffer BCC gmar2 \ Jump to gmar2 (this BCC is effectively a JMP as we \ just passed through a BCS) .gmar1 LDA segmentFlags-120,X \ Set A to the flags for this track segment from the \ track segment buffer .gmar2 AND segmentFlagMask,Y \ Extract the relevant bits of the segment's flags: STA W \ \ W = A AND %00101101 if Y = 0 (right verge) \ %00110011 if Y = 1 (left verge) \ \ So when we are processing the right verge, we extract \ these flags into W while zeroing the rest: \ \ * Bit 0 (section shape) \ * Bit 2 (colour of right verge marks) \ * Bit 3 (show right corner markers) \ * Bit 5 (corner marker colours) \ \ and when we are processing the left verge, we extract \ these flags into W while zeroing the rest: \ \ * Bit 0 (section shape) \ * Bit 1 (colour of left verge marks) \ * Bit 4 (show left corner markers) \ * Bit 5 (corner marker colours) AND #%00000111 \ Set Y = bits 0-2 of W, so Y is in the range 0 to 7, TAY \ where the possible values are as follows: \ \ * Y = 0 = %000 = black right, black left, straight \ * Y = 1 = %001 = black right, black left, curve \ * Y = 2 = %010 = black right, red left, straight \ * Y = 3 = %011 = black right, red left, curve \ * Y = 4 = %100 = red right, black left, straight \ * Y = 5 = %101 = red right, black left, curve \ * Y = 6 = %110 = red right, red left, straight \ * Y = 7 = %111 = red right, red left, curve LDA vergeColour,Y \ When we are processing the right verge, we know bit 1 STA V \ is clear, so the possible values of Y are as follows: \ \ * Y = 0 = %000 = black right, black left, straight \ * Y = 1 = %001 = black right, black left, curve \ * Y = 4 = %100 = red right, black left, straight \ * Y = 5 = %101 = red right, black left, curve \ \ When we are processing the left verge, we know bit 2 \ is clear, so the possible values of Y are as follows: \ \ * Y = 0 = %000 = black right, black left, straight \ * Y = 1 = %001 = black right, black left, curve \ * Y = 2 = %010 = black right, red left, straight \ * Y = 3 = %011 = black right, red left, curve \ \ So if Y = 0 or 1, then we know that the verge we are \ processing is black-and-white, otherwise it is \ red-and-white \ \ These instructions set V to the Y-th entry in the \ vergeColour table, which contains the following: \ \ * 0 when Y = 0 to 1 (black-and-white verge) \ * 1 when Y = 2 to 7 (red-and-white verge) \ \ So V = 0 if this is a black-and-white verge \ 1 if this is a red-and-white verge LDA segmentCounter \ If segmentCounter >= 3 then jump to gmar3 to process CMP #3 \ the segment's corner markers in part 2 BCS gmar3 JMP gmar9 \ Otherwise segmentCounter is 0 to 2, so jump to gmar9 \ to skip the corner markers and move on to the verge \ marks in part 4 \ ****************************************************************************** \ \ Name: GetVergeAndMarkers (Part 2 of 4) \ Type: Subroutine \ Category: Track geometry \ Summary: Calculate the segment's verge width and outside verge coordinates \ \ ****************************************************************************** .gmar3 \ We calculate the verge width as follows: \ \ (U A) = scaleUp * 2 ^ (scaleDown - vergeScale) \ \ to determine the width of the verge marks on the side \ of the track \ \ The higher the value of (U A), the wider the verge for \ this segment \ \ The vergeScale factor is between 3 and 5, and scales \ the verge width differently for different track \ configurations, with larger values of vergeScale \ giving smaller verges \ \ This gives the following: \ \ * If both verges are black-and-white, then the \ verges are thin (vergeScale = 5), on both curved \ and straight sections \ \ * If this is a curve and at least one of the verges \ is red-and-white, or we're on a straight and both \ verges are red-and-white, then the verges are \ medium thickness (vergeScale = 4) \ \ * If this is a straight and only one of the verges \ is red-and-white, then the verges are thick \ (vergeScale = 3) LDA scaleDown \ Set Y = scaleDown - vergeScale SEC SBC vergeScale,Y TAY LDA #0 \ Set U = 0, to use as the high byte in (U A) STA U LDA scaleUp \ Set A = scaleUp \ \ So (U A) = scaleUp DEY \ Set Y = Y - 1 \ = scaleDown - vergeScale - 1 \ We now scale (U A) by 2 ^ Y, so if Y is 0 we don't \ do any scaling, if it's negative we scale down, and \ if it's positive we scale up \ \ Note that the -1 in the scale factor calculation is \ reversed by the right-shift that we apply below when \ setting bit 7 of the shifted result, so the result is \ as above, despite the extra -1 BEQ gmar6 \ If Y = 0, then there is no scaling to be done, so jump \ to gmar6 BPL gmar5 \ If Y > 0, then we need to scale up, so jump to gmar5 \ If we get here then Y < 0, so we need to scale down, \ specifically by right-shifting (U A) by |Y| places .gmar4 LSR U \ Set (U A) = (U A) >> 1 ROR A INY \ Increment the shift counter in Y BNE gmar4 \ Loop back to gmar4 to keep shifting right until we \ have shifted by |Y| places BEQ gmar6 \ Jump to gmar6 (this BEQ is effectively a JMP, as we \ just passed through a BNE) .gmar5 \ If we get here then Y > 0, so we need to scale up, \ specifically by left-shifting (U A) by Y places ASL A \ Set (U A) = (U A) << 1 ROL U DEY \ Decrement the shift counter in Y BNE gmar5 \ Loop back to gmar5 to keep shifting left until we \ have shifted by Y places .gmar6 STA T \ Set (U T) = (U A) \ \ So (U T) contains our scaled value LDA segmentDirection \ Set the C flag to bit 0 of segmentDirection, which LSR A \ will be 0 when our car is facing in the same direction \ as the segment we are checking, or 1 if it's the \ opposite direction ROR A \ Set A = A >> 1 and set bit 7 to the C flag EOR directionFacing \ If the C flag matches directionFacing, jump to gmar7 BPL gmar7 \ If we get here then this is the left verge, so we need \ to negate (U T) so the outside of the verge is to the \ left of the track, i.e. in a negative direction along \ the x-axis LDA #0 \ Negate (U T), starting with the low bytes SEC SBC T STA T LDA #0 \ And then the high bytes SBC U STA U \ So we now have our verge width result: \ \ (U T) = scaleUp * 2 ^ (scaleDown - vergeScale) \ \ where the sign of (U T) is positive for the right \ verge and negative for the left verge .gmar7 LDY segmentListPointer \ Set Y to the index of the current entry in the track \ segment list \ We now calculate the coordinates for the outside edge \ of the track verge by adding the verge width in (U T) \ to the track segment's verge coordinates, storing the \ result in the track segment list, 16 bytes after the \ corresponding track segment entry (so indexes 6 to 21 \ contain the track segment list, while indexes 22 to 37 \ contain the corresponding entries for the outside of \ the verge) LDA xVergeRightLo,Y \ Set (xVergeRightHi+16 xVergeRightLo+16) CLC \ = (xVergeRightHi xVergeRightLo) + (U T) ADC T \ STA xVergeRightLo+16,Y \ starting with the low bytes LDA xVergeRightHi,Y \ And then the high bytes ADC U STA xVergeRightHi+16,Y \ ****************************************************************************** \ \ Name: GetVergeAndMarkers (Part 3 of 4) \ Type: Subroutine \ Category: Track geometry \ Summary: Process the segment's corner markers \ \ ****************************************************************************** LDA W \ If bits 3 and 4 of W are clear, which are these bits AND #%00011000 \ in the segment flags: BEQ gmar9 \ \ * Bit 3 (show right corner markers) \ * Bit 4 (show left corner markers) \ \ then we do not show any corner markers for this \ segment, so jump to gmar9 to move on to the verge \ marks in part 4 \ If we get here then we have a marker to draw for this \ segment LDY markersToDraw \ Set Y to the number of markers we have to draw CPY #3 \ If Y >= 3, then we already have three markers ready BCS gmar9 \ to show, which is the maximum at any one time, so \ jump to gmar9 to skip the following LDA segmentListPointer \ Set markerListIndex for marker Y to segmentListPointer STA markerListIndex,Y LDA W \ Set markerData for marker Y to the segment flags for STA markerData,Y \ this marker in W AND #1 \ If bit 0 of W is clear, then this is a straight track BEQ gmar8 \ section, so jump to gmar8 to skip the following \ instruction \ This is a curved section, so move the markers closer \ to the track edge by halving the distance that we \ store in xMarker LSR U \ Set (U T) = (U T) >> 1 ROR T .gmar8 LDA T \ Set (xMarkerHi xMarkerLo) for marker Y to (U T), so STA xMarkerLo,Y \ xMarker contains the width of the verge (halved if LDA U \ this is a corner), which we can use as the x-axis STA xMarkerHi,Y \ distance from the track verge to the marker INC markersToDraw \ Increment markersToDraw, as we have just added a new \ marker to the list \ ****************************************************************************** \ \ Name: GetVergeAndMarkers (Part 4 of 4) \ Type: Subroutine \ Category: Track geometry \ Summary: Store details of the segment's verge marks \ \ ****************************************************************************** .gmar9 \ The verge marks are either black-white-black-white \ or red-white-red-white, so we now work out which of \ these colours applies to this segment TXA \ If bit 0 of X is clear, then this is a non-white verge AND #%00000001 \ mark, so jump to gmar10 to set A = V to use as the BEQ gmar10 \ vergeDataRight for this segment LDA #2 \ Otherwise this is a white verge mark, so set A = 2 \ to use as the vergeDataRight for this segment BNE gmar11 \ Jump to gmar11 (this BNE is effectively a JMP as A is \ never zero) .gmar10 LDA V \ Set A = V, which is 0 (black verge mark) or 1 (red \ verge mark) .gmar11 LDY segmentListPointer \ Set Y to the index of the current entry in the track \ segment list STA vergeDataRight,Y \ Store A in the segment's corresponding vergeDataRight, \ so that's 2 for a white verge mark, 1 for a red verge \ mark, and 0 for a black verge mark LDA LL \ Set A to the segment's pitch angle, from the point \ of view of the player STA yVergeRight,Y \ Store the result in the segment's entry in yVergeRight \ to set the segment's pitch angle CMP #80 \ If the pitch angle is 80 or more, jump to gmar12 BCS gmar12 \ to return from the subroutine CMP horizonLine \ If the pitch angle is less than horizonLine, jump BCC gmar12 \ to gmar12 to return from the subroutine \ If we get here then the pitch angle in A is less \ than 80 and is greater or equal to horizonLine STA horizonLine \ This track segment is higher than the current horizon \ pitch angle, so the track obscures the horizon and we \ need to update horizonLine to this new pitch angle STY horizonListIndex \ Set horizonListIndex to the track segment number in Y .gmar12 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: HideAllCars \ Type: Subroutine \ Category: Car geometry \ Summary: Set all the cars to hidden \ \ ****************************************************************************** .HideAllCars LDX #22 \ We are about to process the car status bytes for \ drivers 0 to 19, plus the three extra car objects in \ 20 to 22 that make up the four-object car, so set a \ loop counter in X .hide1 LDA objectStatus,X \ Set bit 7 in the X-th byte of objectStatus to set the ORA #%10000000 \ car for driver X to be hidden STA objectStatus,X DEX \ Decrement the loop counter BPL hide1 \ Loop back until we have hidden all 23 cars RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: Delay \ Type: Subroutine \ Category: Main loop \ Summary: Delay for a specified number of loops \ \ ------------------------------------------------------------------------------ \ \ This routine performs T + (5 * 256) loop iterations, to create a delay. The \ value of T doesn't have much effect on the amount of delay, so it looks like \ this variable was chosen simply because it doesn't contain anything useful at \ this point. \ \ ****************************************************************************** .Delay LDX #6 \ Set X as the counter for the outer loop .dely1 DEC T \ Loop around for T iterations in the inner loop BNE dely1 DEX \ Loop around for X iterations in the outer loop BNE dely1 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: MoveAndDrawCars \ Type: Subroutine \ Category: Car geometry \ Summary: Move the cars around the track and draw any that are visible, up \ to a maximum of five \ \ ****************************************************************************** .MoveAndDrawCars LDA qualifyingTime \ If bit 7 of qualifyingTime is set then this is a BMI Delay \ practice lap (i.e. qualifyingTime = 255), so there are \ no other cars to draw \ \ To maintain the same game speed as for races, we jump \ to Delay to pause for a while before returning from \ the subroutine using a tail call LDX positionBehind \ Set X to the position of the driver behind us LDY driversInOrder,X \ Set Y to the number of the driver in behind us LDA objectStatus,Y \ Clear bit 7 of the car object's status byte, to flag AND #%01111111 \ the car behind us as being visible STA objectStatus,Y JSR MoveCars \ Move the cars around the track JSR ProcessOvertaking \ Process overtaking manoeuvres for the non-player \ drivers JSR HideAllCars \ Set all the cars to be hidden JSR SetPlayerPositions \ Set the current player's position, plus the position \ ahead and the position behind LDX currentPosition \ Set X to the current player's position LDY #5 \ We now work our way through the five nearest cars in \ front of us, so set a loop counter in Y .dcar1 BIT directionFacing \ If bit 7 of directionFacing is clear, then we are BPL dcar2 \ facing forwards, so jump to dcar2 JSR GetPositionBehind \ We are facing backwards, so set X to the number of \ the position behind position X, to get the number of \ the car that we are looking at JMP dcar3 \ Jump to dcar3 to skip the following .dcar2 JSR GetPositionAhead \ We are facing forwards, so set X to the number of the \ position ahead of position X, to get the number of \ the car that we are looking at .dcar3 STY thisDriverNumber \ Store the loop counter in thisDriverNumber so we can \ retrieve it after the following call STX thisPosition \ Store the position of the car we are considering in \ thisPosition JSR BuildVisibleCar \ Build the car object if it is visible, so we can draw \ it below LDX thisPosition \ Retrieve the position of the car that we stored in \ thisPosition above LDY thisDriverNumber \ Retrieve the value of the loop counter that we stored \ in thisDriverNumber above DEY \ Decrement the loop counter BPL dcar1 \ Loop back until we have processed five cars in front JSR DrawCars \ Draw all the cars, with the closest car in front of us \ split into four objects LDX positionBehind \ Set X to the position of the driver behind us JSR BuildVisibleCar \ Build the car object if it is visible, so it can be \ shown in the mirror if close enough RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: SwapDriverPosition \ Type: Subroutine \ Category: Drivers \ Summary: Swap the position for two drivers (i.e. overtake) \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The first position \ \ Y The second position \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ X The number of the driver now at position X \ \ Y The number of the driver now at position Y \ \ ****************************************************************************** .SwapDriverPosition LDA driversInOrder,X \ Set T to the number of the driver at position X STA T LDA driversInOrder,Y \ Set A to the number of the driver at position Y STA driversInOrder,X \ Set the driver at position X to the driver from \ position Y TAX \ Set X to the number of the driver now at position X LDA T \ Set the driver at position y to the driver from STA driversInOrder,Y \ position X TAY \ Set Y to the number of the driver now at position Y RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: ProcessOvertaking (Part 1 of 3) \ Type: Subroutine \ Category: Tactics \ Summary: Process all cars for overtaking manoeuvres, checking first to see \ if the car has just finished overtaking the car in front \ Deep dive: Tactics of the non-player drivers \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ H Gets a 1 rotated left into bit 0 each time we process \ one car overtaking another \ \ ****************************************************************************** .ProcessOvertaking LDX currentPosition \ Set X to the current player's position, to use as a \ loop counter in the following as we work backwards \ through the field from this position .tact1 STX W \ Store the position number in W, so we can retrieve it \ during the loop LDA driversInOrder,X \ Set T to the number of the driver in position X STA T JSR GetPositionAhead \ Set X to the number of the position ahead of position \ X LDA driversInOrder,X \ Set G to the number of the driver in position X, i.e. \ the number of the driver ahead of driver T STX G \ Store the position number of the driver ahead in G, so \ we can retrieve it during the loop TAY \ Set Y to the number of the driver ahead of driver T LDX T \ Set X to the number of the driver we are currently \ processing \ So in the following, we are applying driving tactics \ to driver X (aka driver T) \ \ We start by comparing driver X with the driver ahead, \ driver Y \ \ Drivers X and Y are in positions W and G respectively LDA #0 \ Set N = 0, which we will use to build the car status STA N \ flags for driver X STA carSteering,X \ Set this driver's carSteering to 0, so by default \ driver X will drive straight (though we may change \ this below) JSR CompareCarSegments \ Set A to the number of segments between drivers X and \ Y (i.e. the race distance between the cars) BCS tact4 \ If the C flag is set then the cars are far apart, so \ jump to tact18 via tact4 to update the car status byte \ for driver X to N = 0, and then move on to the next \ driver BPL tact5 \ If the distance in A is positive then driver Y is \ still ahead of driver X, so jump to tact5 to apply \ tactics to driver X in part 2 \ If we get here then driver Y is not actually in front \ of this driver, despite being in a higher position, so \ we now need to check how far ahead driver X is CMP #&F6 \ If A < -10, driver X is not very far ahead of driver BCC tact4 \ Y, so we don't yet consider this a passing move, so \ jump to tact18 via tact4 to update the car status byte \ for driver X to N = 0, and then move on to the next \ driver \ If we get here then driver X has overtaken driver Y, \ and the distance between the cars is >= 10, so we need \ to swap their positions, as driver X has pulled far \ enough away for this to be considered a passing move LDX W \ Swap the drivers between positions W and G, i.e. swap LDY G \ the positions of driver X and Y, so driver X moves JSR SwapDriverPosition \ into a higher position, and set: \ \ * X = the number of the driver now at position W, \ i.e. the driver behind, previously referred to \ as driver Y \ \ * Y = the number of the driver now at position G, \ i.e. the driver ahead, previously referred to \ as driver X \ \ So now X and Y have swapped, so driver Y just passed \ driver X SEC \ Set bit 7 of updateDriverInfo so the driver names get ROR updateDriverInfo \ updated at the top of the screen, to reflect the new \ race positions CPY currentPlayer \ If driver Y (the one that just did the overtaking) BNE tact2 \ is not the current player, jump to tact2 to skip the \ following two instructions \ If we get here then the C flag is set, as the above \ comparison is equal \ Driver Y (the one that just did the overtaking) is the \ current player, so we now need to reduce the current \ player's position by 1 to move them into a higher \ position LDA #&99 \ Set A = -1 in BCD, which we can use to decrement the \ BCD number in positionChangeBCD below, so the current \ player's position will go down by 1 BNE tact3 \ Jump to tact3 (this BNE is effectively a JMP as A is \ never zero) .tact2 CPX currentPlayer \ If driver X (the one that just got overtaken) is not BNE tact4 \ the current player, jump to tact18 via tact4 to update \ the car status byte for this driver to N = 0, and then \ move on to the next driver \ If we get here then the C flag is set, as the above \ comparison is equal \ Driver Y (the one that just did the overtaking) is the \ current player, so we now need to reduce the current \ player's position by 1 to move them into a higher \ position LDA #&01 \ Set A = 1 in BCD, which we can use to increment the \ BCD number in positionChangeBCD below, so the current \ player's position will go up by 1 .tact3 STA T \ Set T = A, so A contains the position change in BCD LDA driverLapNumber,Y \ Set A to the lap number for the driver ahead ROL H \ Rotate the C flag into bit 0 of H, which we know is \ set from the comparisons above, so this rotates a 1 \ into bit 0 of H (though this doesn't appear to be used \ anywhere, so this instruction is a bit of a mystery) SBC driverLapNumber,X \ Subtract the lap number for this driver BNE tact4 \ If the drivers are on different laps, jump to tact18 \ via tact4 to update the car status byte for this \ driver to N = 0, and then move on to the next driver SED \ Set the D flag to switch arithmetic to Binary Coded \ Decimal (BCD) CLC \ Set positionChangeBCD = positionChangeBCD + T LDA T \ ADC positionChangeBCD \ so this applies the position change we calculated STA positionChangeBCD \ above to positionChangeBCD CLD \ Clear the D flag to switch arithmetic to normal .tact4 JMP tact18 \ Jump to tact18 to update the car status byte for \ driver X to N = 0, and then move on to the next driver \ ****************************************************************************** \ \ Name: ProcessOvertaking (Part 2 of 3) \ Type: Subroutine \ Category: Tactics \ Summary: The car we are processing has not overtaken the car in front of \ it, so if applicable, we can keep manoeuvring into position \ \ ****************************************************************************** .tact5 \ We jump here with the distance between driver X and \ driver Y in A, which is positive as driver Y is ahead \ of driver X CMP #5 \ If A >= 5, then the cars are not very close, so jump BCS tact4 \ to tact18 via tact4 to update the car status byte for \ this driver to N = 0, and then move on to the next \ driver LDA carSpeedLo,X \ Set A to the high byte of the following subtraction: CLC \ SBC carSpeedLo,Y \ driver X speed - driver Y speed - 1 LDA carSpeedHi,X SBC carSpeedHi,Y ROR V \ Rotate the C flag into bit 7 of V, so bit 7 is set if \ driver X is going faster than driver Y (so driver X is \ catching up), and it's clear if driver Y is running \ away with it BPL tact4 \ If bit 7 of the ROR result is clear, i.e. the C flag \ is clear, i.e. if driver Y is driving as fast as or \ faster than driver X, jump to tact18 via tact4 to \ update the car status byte for this driver to N = 0, \ and then move on to the next driver \ If we get here then driver X is behind driver Y but is \ driving faster than driver Y, so we need to think \ about steering driver X \ \ We also know that A is positive, as the above \ subtraction didn't underflow LSR A \ Set A = A / 2 CMP #30 \ If A < 30, jump to tact6 to skip the following BCC tact6 \ instruction LDA #30 \ Set A = 30, so A is a maximum of 30 .tact6 CMP #4 \ If A >= 4, jump to tact7 to skip the following BCS tact7 \ instruction LDA #4 \ Set A = 4, so A is a minimum of 4 .tact7 STA SS \ Store A in SS, which we will use below as the amount \ of steering to apply, in the range 4 to 30, with more \ steering being applied when the cars have a bigger \ speed gap LDA T \ Set A to the number of the driver we are currently \ processing, which we stored in T in part 1 (though the \ same number is still in X, so this could be done more \ efficiently with a TXA instruction) CMP #4 \ Set the C flag if A >= 4, clear the C flag if A < 4 LDA carStatus,Y \ Set A to just bit 6 of driver Y's car status byte, AND #%01000000 \ which is the acceleration flag BEQ tact9 \ If bit 6 of driver Y's car status byte is clear, then \ driver Y is not accelerating, so jump to tact9 \ If we get here then driver Y is accelerating, so we \ get driver X to follow driver Y's racing line BCS tact8 \ If the C flag is set, then the driver number of the \ driver we are currently processing is 4 or greater, so \ jump to tact8 to skip the following instruction ORA #%10000000 \ We are currently processing one of drivers 0 to 3, who \ are the four best drivers, so set bit 7 of A to apply \ the brakes, overriding the acceleration flag in bit 6 .tact8 STA N \ Store the updated flags in N, so bit 6 of driver X \ matches driver Y's bit 6 (so their acceleration status \ matches), and bit 7 (braking) set if X = 0 to 3 LDA carRacingLine,X \ If the racing line for driver X >= the racing line for CMP carRacingLine,Y \ driver Y, set the C flag, otherwise clear it \ \ In other words, the C flag is set if driver X is to \ the left of driver Y ROR T \ Rotate the C flag into bit 7 of T, so we can use this \ bit to determine the direction that driver X should \ steer \ \ This steers driver X to the right (bit 7 set) when \ driver X is to the left of driver Y - in other words, \ it steers driver X towards driver Y's racing line, \ into driver Y's slipstream JMP tact15 \ Jump to tact15 to apply the steering direction in T to \ the amount of steering in SS .tact9 \ If we get here then driver Y is not accelerating, so \ we get driver X to move to overtake driver Y BCS tact10 \ If the C flag is set, then the driver number of the \ driver we are currently processing is 4 or greater, so \ jump to tact10 LDA #%01000000 \ Set N so it only has bit 6 set (so driver X will be STA N \ set to accelerate) LDA carRacingLine,Y \ If the racing line for driver Y >= the racing line for CMP carRacingLine,X \ driver X, set the C flag, otherwise clear it \ \ In other words, the C flag is set if driver X is to \ the right of driver Y ROR T \ Rotate the C flag into bit 7 of T, so we can use this \ bit to determine the direction that driver X should \ steer \ \ This steers driver X to the right (bit 7 set) when \ driver X is to the right of driver Y - in other words, \ it steers driver X away from driver Y's racing line, \ into an overtaking position AND #&FF \ This instruction doesn't change any values, but it \ does set the N flag according to the current value of \ A, so the call to Absolute8Bit will return |A| rather \ than being affected by the result of the ROR \ instruction JSR Absolute8Bit \ Set A = |A| \ \ As A is the racing line, this gives the distance of \ driver Y from the edge of the track, with 0 being on \ the verge, and 127 being in the centre CMP #60 \ If A < 60, then driver Y is close to the verge, so BCC tact11 \ jump to tact11 to steer driver X into the other half \ of the track to driver Y BCS tact12 \ Otherwise we steer driver X away from driver Y's \ racing line, by jumping to tact12 (this BCS is \ effectively a JMP as we just passed through a BCC) .tact10 LSR V \ Clear bit 7 of V, so we never end up applying the \ brakes in tact12 or tact14 below .tact11 LDA carRacingLine,Y \ Set T to the racing line for driver Y, specifically STA T \ so we can extract the top bit to determine which side \ of the track driver Y is on (0 = right, 1 = left), \ and to steer in the opposite direction .tact12 LDA objectStatus,X \ If bit 7 of driver X's object status byte is clear, BPL tact13 \ then the car is visible, so jump to tact13 \ If we get here then driver X is not visible 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 #31 \ Reduce the random number to the range 0 to 31 BNE tact18 \ If A is non-zero, jump to tact18 to update the car \ status byte for this driver to N, and then move on \ to the next driver \ If we get here then A is zero, which has a 3.125% \ chance of happening LDA V \ Set A to N, but with bit 7 set to bit 7 of V, so bit 7 AND #%10000000 \ of N gets set if driver X is going faster than driver ORA N \ Y, which means driver X slams on the brakes JMP tact17 \ Jump to tact17 to update the car status byte for this \ driver to the value of A, and then move on to the next \ driver .tact13 \ If we get here then driver X is visible LDA carRacingLine,Y \ Set A to the difference between the racing lines for SEC \ driver X and driver Y SBC carRacingLine,X BCS tact14 \ If the subtraction didn't underflow, jump to tact14 to \ skip the following instruction EOR #&FF \ The subtraction underflowed, so flip all the bits in \ the result to change it from negative to positive, so \ A contains the difference between the two drivers' \ racing lines, made positive .tact14 CMP #100 \ If A >= 100, then the cars are far apart in terms of BCS tact18 \ left-right spacing, so jump to tact18 to skip applying \ any steering, and instead just update the car status \ byte for this driver to N, before moving on to the \ next driver CMP #80 \ If A >= 80, then the cars are slightly closer, so jump BCS tact16 \ to tact16 to set bit 4 of driver X's car status byte \ (so driver X applies the steering we've been \ calculating) CMP #60 \ If A >= 60, then the cars are even closer, so jump to BCS tact15 \ tact15 to steer driver X in the direction in T, which \ we set above to the side of the track driver Y is on \ (0 = right, 1 = left) \ \ This therefore steers driver X away from driver Y, as \ in terms of steering 0 = steer left, 1 = steer right \ If we get here then the cars are really close, so we \ get driver X to slam on the brakes, as well as \ steering away from driver Y LDA V \ Store the bit 7 of V in bit 7 of the car status flag AND #%10000000 \ byte we are building in N, so driver X applies the ORA N \ brakes when bit 7 of V is set STA N .tact15 LDA T \ Set the steering for this driver to SS, with the top AND #%10000000 \ bit (i.e. the direction of the steering) set to the ORA SS \ sign bit of T (0 = steer left, 1 = steer right) STA carSteering,X .tact16 LDA N \ Set bit 4 of the car status flag byte we are building ORA #%00010000 \ in N, so the car does not automatically follow the \ segment's steering line in segmentSteering, and \ instead applies the steering we've been calculating .tact17 STA N \ Store A in N to use as the car status flags for this \ driver \ ****************************************************************************** \ \ Name: ProcessOvertaking (Part 3 of 3) \ Type: Subroutine \ Category: Tactics \ Summary: Update the car status (if configured) and loop back for the next \ car \ \ ****************************************************************************** .tact18 LDA carStatus,X \ Set the C flag to bit 0 of the car status flags for LSR A \ this driver LDA N \ Set A = N, to use as the new car status flags for this \ driver, but only when bit 0 of the current flag byte \ is set BCS tact19 \ If the C flag is set, i.e. bit 0 of the car status \ flags for this driver is set, jump to tact19 to skip \ the following instruction STA carStatus,X \ Set the car status flags for this driver to A (i.e. to \ the flags in N) .tact19 LDX W \ Set X to the position that we just checked, which we \ stored in W at the start of the loop JSR GetPositionBehind \ Set X to the number of the position behind position X, \ so we work backwards through the field CPX currentPosition \ If X = the current player's position, jump to tact20 BEQ tact20 \ to return from the subroutine JMP tact1 \ Otherwise jump back to tact1 to process the next \ driver in X .tact20 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: CompareCarSegments \ Type: Subroutine \ Category: Car geometry \ Summary: Calculate the distance between two cars, in terms of segments and \ progress with the current segment \ Deep dive: Placing cars on the track \ \ ------------------------------------------------------------------------------ \ \ Extends the distance calculation in CompareSegments by rounding the segment \ numbers used in the calculations, depending on the cars' progress within their \ current segments. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The number of driver X \ \ Y The number of driver Y \ \ ****************************************************************************** .CompareCarSegments LDA carProgress,Y \ Set the C flag according to the subtraction: SEC \ SBC carProgress,X \ carProgress for driver ahead \ - carProgress for this driver \ \ The progress figures act like a fractional byte \ in the subtraction at the start of the following \ routine, though because only the C flag is kept, they \ only serve to round the result to the nearest integer, \ rather than giving a full 24-bit result \ Fall through into CompareSegments to calculate the \ distance between the two cars, rounding the result \ according to the difference in the cars' progress \ values \ ****************************************************************************** \ \ Name: CompareSegments \ Type: Subroutine \ Category: 3D objects \ Summary: Calculate the distance between two objects, in terms of the \ difference in their segment numbers \ Deep dive: Placing cars on the track \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The number of object X \ \ Y The number of object Y \ \ C flag Determines the accuracy of the arithmetic: \ \ * Clear for a 16-bit calculation using objectSegment \ \ * For a 24-bit calculation, contains the carry from \ CompareCarSegments above \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ A The distance between the two objects, negative if object \ X is ahead, positive if object Y is ahead \ \ T The same as A \ \ C flag How far apart the objects are: \ \ * Set if objects are far apart (distance >= 128) \ \ * Clear if they are close (distance < 128) \ \ N flag The object order (if the objects are close): \ \ * Set if object X is ahead by more than 256 \ \ * Clear otherwise \ \ H Relationship to the starting line: \ \ * Bit 7 is clear if the object are quite close (when \ distance < 256) but are on opposite sides of the \ starting line \ \ * Bit 7 set otherwise \ \ ****************************************************************************** .CompareSegments LDA objectSegmentLo,Y \ Set (A T) = objectSegment for object Y SBC objectSegmentLo,X \ - objectSegment for object X STA T \ \ starting with the low bytes LDA objectSegmentHi,Y \ And then the high bytes SBC objectSegmentHi,X \ \ So (A T) now contains the difference between the two \ objects in terms of their segment numbers, i.e. the \ distance between the two in terms of segment numbers \ \ Let's call this dSegments PHP \ Store the status register on the stack, so the N flag \ on the stack is the sign of the above subtraction, so \ it's set if objectSegmentHi for object Y < \ objectSegmentHi for object X, i.e. if object X is \ ahead by 256 segments or more BPL dist1 \ If the result of the subtraction was positive, jump \ to dist1 to skip the following instruction JSR Absolute16Bit \ The result of the subtraction was negative, so set \ (A T) = |A T| .dist1 STA U \ Set (U T) = (A T) \ = |dSegments| SEC \ Set the C flag to shift into bit 7 of H below BEQ dist2 \ If the high byte of the segment difference is zero, \ jump to dist2 to check the low byte \ If we get here then the high byte of the difference \ is non-zero, so now we need to check whether this is \ down to the objects being close but either side of the \ starting line \ \ This is because the segment number resets to zero at \ the starting line, so objects that are on either side \ of the line will have a big difference in their \ segment numbers even though they are actually quite \ close together PLA \ Flip the N flag in the status register on the stack EOR #%10000000 \ (as the N flag is bit 7 of the status register), so PHP \ the N flag on the stack is the opposite sign to the \ objectSegment subtraction we did above, in other words \ it's now clear if object X is ahead by 256 or more and \ set otherwise \ \ This caters for the situation where the cars are close \ but on either side of the starting line, in which case \ the subtraction we did above will give the wrong \ result, so this flips the order of the two objects to \ be correct \ In the following, the 16-bit number at trackLength \ contains the length of the full track in terms of \ segments LDA trackLength \ Set (A T) = trackLength - (U T) SEC \ = trackLength - |dSegments| SBC T \ STA T \ starting with the low bytes LDA trackLength+1 \ And then the high bytes SBC U BNE dist3 \ If the high byte is non-zero, then that means the \ objects are not just either side of the starting line, \ so they must be far away from each other, so jump to \ dist3 to return from the subroutine with the C flag \ set \ If we get here then the high byte is zero, which means \ the objects are quite close but are either side of the \ starting line CLC \ Clear the C flag .dist2 \ If we get here then the objects are close together \ (the high byte of the difference in segment numbers is \ zero) ROR H \ Set bit 7 of H to the C flag, so it's clear if the \ objects are quite close but on opposite sides of the \ starting line, otherwise it is set LDA T \ If T >= 128, then the difference between the objects CMP #128 \ is >= 128 segments, so jump to dist3 to return from BCS dist3 \ the subroutine with the C flag set \ If we get here then the difference between the objects \ is 127 or less, so the objects are determined to be \ close PLP \ Retrieve the status flags from the stack JSR Absolute8Bit \ Multiply the sign of A by the N flag STA T \ Set T = A LDA T \ Set the N flag according to the value of A CLC \ Clear the C flag RTS \ Return from the subroutine .dist3 \ We get here if the objects are far away from each \ other, i.e. the segment difference between them is \ >= 128 PLP \ Retrieve the status flags from the stack, to return \ from the subroutine SEC \ Set the C flag RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: MoveCars (Part 1 of 2) \ Type: Subroutine \ Category: Car geometry \ Summary: Move the cars around the track \ Deep dive: Placing cars on the track \ Tactics of the non-player drivers \ \ ------------------------------------------------------------------------------ \ \ This part changes each car's speed. It calculates the speed change in (U A), \ and then applies it to the car's speed. It then moves the car round the track \ by the speed we just calculated by updating carProgress. \ \ ------------------------------------------------------------------------------ \ \ Other entry points: \ \ MoveCars-1 Contains an RTS \ \ ****************************************************************************** .MoveCars LDA raceStarting \ If bit 7 of raceStarting is set, then the race is in BMI MoveCars-1 \ the process of starting and we are on the starting \ grid, so the cars are not allowed to move, so return \ from the subroutine (as MoveCars-1 contains an RTS) LDX #20 \ Set X = 20 to use as a loop counter as we work through \ all 20 cars JMP mcar20 \ Jump into the loop at mcar20 to decrement X and start \ looping through the drivers, looping back to mcar1 for \ all drivers except the current player .mcar1 \ This first part of the loop changes the car's speed as \ required LDA carStatus,X \ If bit 7 of driver X's carStatus is set, jump to BMI mcar8 \ mcar8 to apply the brakes LDY objTrackSection,X \ Set Y to the track section number * 8 for driver X LDA trackSectionFlag,Y \ Set A to the flag byte for the driver's track section BPL mcar2 \ If bit 7 of this section's flag byte is clear, jump to \ mcar2 \ If we get here then bit 7 of this section's flag byte \ is set LDA carSpeedHi,X \ If the high byte of the car's speed >= the car's CMP carSectionSpeed,X \ carSectionSpeed then jump to mcar11 to skip changing BCS mcar11 \ the car's speed, as it is already going fast enough \ for the section BCC mcar3 \ Otherwise jump to mcar3 to continue with the speed \ calculation (this BCC is effectively a JMP as we just \ passed through a BCS) .mcar2 \ If we get here then bit 7 of this section's flag byte \ is clear, and A contains this section's flag byte LSR A \ Set the C flag to bit 0 of A, i.e. to bit 0 of this \ section's flag byte BCS mcar3 \ If bit 0 of this section's flag byte is set, then this \ is a curved section, so jump to mcar3 to continue with \ the speed calculation \ If we get here then bits 0 and 7 of this section's \ flag byte are both clear, so this is a straight \ section and there is no maximum speed set LDA trackDriverSpeed,Y \ Set carSectionSpeed for this driver to the value of STA carSectionSpeed,X \ trackDriverSpeed for this track section CLC \ Set A = trackDriverSpeed - carSpeedHi - 1 SBC carSpeedHi,X BCS mcar3 \ If the subtraction didn't underflow, then \ trackDriverSpeed > carSpeedHi, so jump to mcar3 to \ continue with the speed calculation LSR A \ Set T = A >> 2 with bits 6 and 7 set LSR A \ ORA #%11000000 \ As A is negative, this divides A by 4 while keeping STA T \ the sign, so: \ \ T = A / 4 \ = (trackDriverSpeed - carSpeedHi - 1) / 4 LDA objSectionSegmt,X \ Set A = objSectionSegmt - trackSectionTurn SEC \ SBC trackSectionTurn,Y \ so this takes the driver's current segment number in \ the track section and subtracts the trackSectionTurn \ for this track section BCS mcar11 \ If the subtraction didn't underflow, then \ objSectionSegmt >= trackSectionTurn, so jump to mcar11 \ to skip changing the car's speed CMP T \ If A >= T, jump to mcar8 to apply the brakes BCS mcar8 .mcar3 \ We now set A and T to use in the calculation to work \ out the level of acceleration we need to apply to the \ car's speed LDA carSpeedHi,X \ Set A to the high byte of the car's current speed CMP #60 \ If the high byte of the car's speed in A >= 60, jump BCS mcar4 \ to mcar4 to skip the following instruction LDA #22 \ Set A = 22 .mcar4 \ By this point, A is either 22 or >= 60 STA T \ Set T = A LDA carStatus,X \ If bit 6 of driver X's carStatus is clear, then we do AND #%01000000 \ not accelerate the car, so jump to mcar5 with A = 0 BEQ mcar5 LDA #5 \ Set A = 5 .mcar5 \ By this point \ \ * T is carSpeedHi, reduced to 22 if < 60 \ \ * A is either 0 or 5, depending on bit 6 of driver \ X's carStatus, i.e. whether the car is set to be \ accelerating CLC \ Set A = A + the driver's average speed, as calculated ADC driverSpeed,X \ in the SetDriverSpeed routine \ So by this point: \ \ * T is carSpeedHi, reduced to 22 if < 60 \ \ * A is driver X's average speed, + 5 if driver X is \ accelerating \ We now apply any trackRaceSlowdown factor from the \ track data, which allows us to slow down races for \ debugging purposes (trackRaceSlowdown is set to 0 in \ in the Silverstone track, so this has no effect) BIT raceStarted \ If bit 7 of raceStarted is clear then this is practice BPL mcar6 \ or qualifying, so jump to mcar6 to skip the following \ instruction SBC trackRaceSlowdown \ This is a race, so set A = A - trackRaceSlowdown \ \ The value of trackRaceSlowdown for the Silverstone \ track is zero, so this has no effect, but if it were \ non-zero then it would reduce the speed of all cars \ in a race by that amount .mcar6 \ We now calculate (U A) = A - T to get the speed change \ to apply to the driver, given the following values: \ \ * T is carSpeedHi, reduced to 22 if < 60 \ \ * A is driver X's average speed, + 5 if driver X is \ accelerating, reduced by trackRaceSlowdown if this \ is a race \ \ In other words, T is the current speed, while A is the \ speed we should be aiming for, so \ \ (U A) = A - T \ \ will give us the delta that we need to apply to the \ car's current speed to get to the new speed, with the \ acceleration much higher when the car's current speed \ is < 60 (when T is reduced to 22) \ \ So cars can't accelerate fast once they pass a certain \ speed (carSpeedHi >= 60) LDY #0 \ Set Y = 0, so (Y A) = A SEC \ Set (Y A) = (Y A) - T SBC T \ = A - T \ \ starting with the low bytes BCS mcar7 \ And then the high bytes DEY .mcar7 STY U \ Set (U A) = (Y A) \ = A - T JMP mcar9 \ Jump to mcar9 .mcar8 \ If we get here then we are applying the brakes, so \ set (U A) to -256 so we subtract 1024 from the speed \ in the following LDA #&FF \ Set (U A) = -256 (&FF00) STA U \ LDA #0 \ so the following adds -1024 to the car speed .mcar9 \ By this point we have calculated the speed change in \ (U A), so now we apply it ASL A \ Set (U A) = (U A) * 4 ROL U ASL A ROL U CLC \ Set (A carSpeedLo) = (carSpeedHi carSpeedLo) + (U A) ADC carSpeedLo,X \ STA carSpeedLo,X \ starting with the low bytes LDA U \ And then the high bytes ADC carSpeedHi,X CMP #190 \ If the high byte in A < 190, jump to mcar10 to store BCC mcar10 \ the result in carSpeedHi LDA #0 \ Otherwise the car's speed is now a negative value, so STA carSpeedLo,X \ we zero the car's speed, starting with the low byte, \ and setting A = 0 so we also zero the high byte in the \ next instruction .mcar10 STA carSpeedHi,X \ Update the high byte of the car's speed to A .mcar11 LDA #1 \ Set V = 1, so we do the following loop twice, which STA V \ updates the car's progress by 2 x carSpeedHi .mcar12 LDA carSpeedHi,X \ Add carSpeedHi to carProgress to move the car along CLC \ the track by its speed ADC carProgress,X STA carProgress,X BCC mcar13 \ If the addition didn't overflow, jump to mcar13 to do \ the next loop JSR MoveObjectForward \ The addition overflowed, so carProgress has filled up \ and we need to move the car forwards by one segment .mcar13 DEC V \ Decrement the loop counter in V BPL mcar12 \ Loop back until we have added carSpeedHi twice \ ****************************************************************************** \ \ Name: MoveCars (Part 2 of 2) \ Type: Subroutine \ Category: Car geometry \ Summary: Move the cars forward around the track, and apply steering \ Deep dive: Placing cars on the track \ Tactics of the non-player drivers \ \ ------------------------------------------------------------------------------ \ \ This part moves the car round the track by the speed we just calculated, and \ applies steering if required. \ \ The steering algorithm works as follows: \ \ * Only apply steering if the car is visible. \ \ * If any of the following are true, apply the amount of steering given in \ carSteering: \ \ * Bit 6 of the car's objectStatus is set \ \ * Bit 6 of the car's carSteering is clear \ \ * Bit 7 of the car's racing line = bit 7 of carSteering, so: \ \ * Car is in the right half and is steering left (when bit 7 is clear) \ \ * Car is in the left half and is steering right (when bit 7 is set) \ \ * The car is not within 30 of either verge, 30 <= carRacingLine < 226 \ \ * Otherwise, if the car's racing line is within 20 of either verge, steer \ away from the verge by one. \ \ ****************************************************************************** LDA objectStatus,X \ If bit 7 of the driver's car object status byte is ASL A \ set, then the car is not visible, so jump to mar20 to BCS mcar20 \ move on to the next car BMI mcar17 \ If bit 6 of the driver's car object status byte is \ set, then the car has finished racing, so jump to \ mcar17 to steer the car LDA carSteering,X \ If bit 6 of the car's carSteering is clear, jump to AND #%01000000 \ mcar17 to steer the car BEQ mcar17 LDA carRacingLine,X \ If bit 7 of the car's racing line and carSteering are EOR carSteering,X \ the same (so the car is steering into the opposite BPL mcar17 \ half of the track), jump to mcar17 to steer the car LDA carRacingLine,X \ If the car's racing line < 128, then the car is in the BPL mcar15 \ right half of the track, so jump to mcar15 to check \ how close it is to the right verge CMP #236 \ If the car's racing line < 236, jump to mcar14 BCC mcar14 \ If we get here then the car's racing line >= 236, \ which is very close to the left verge, so we steer a \ little to the right DEC carRacingLine,X \ Steer the car a little to the right BCS mcar20 \ Jump to mcar20 (this BCS is effectively a JMP as we \ just passed through a BCC) .mcar14 CMP #226 \ If the car's racing line < 226, jump to mcar17 to BCC mcar17 \ steer the car \ If we get here, then the car's racing line >= 226, \ which is quite close to the left verge BCS mcar20 \ Jump to mcar20 (this BCS is effectively a JMP as we \ just passed through a BCC) .mcar15 CMP #20 \ If the car's racing line >= 20, jump to mcar16 BCS mcar16 \ If we get here then the car's racing line < 20, which \ is very close to the right verge, so we steer a little \ to the left INC carRacingLine,X \ Steer the car a little to the left BCC mcar20 \ Jump to mcar20 (this BCC is effectively a JMP as we \ just passed through a BCS) .mcar16 CMP #30 \ If the car's racing line < 30, jump to mcar20 to move BCC mcar20 \ on to the next driver .mcar17 \ If we get here, then we need to apply the steering \ given in carSteering \ \ carSteering is a sign-magnitude number, where bit 7 is \ the sign and bits 0-5 contain the magnitude, so before \ we can apply the steering, we need to convert it into \ a signed number LDA carSteering,X \ Set A to the car's carSteering AND #%10111111 \ Clear bit 6 CLC \ Clear the C flag in preparation for the addition below BPL mcar18 \ If bit 7 of A is clear, then the amount of steering is \ positive and the number is already correct, so jump to \ mcar18 to add it to the racing line \ If we get here then the sign-magnitude number in A is \ negative, so we need to convert this into a signed \ number \ \ We do this by first setting bit 6 (which we just \ cleared above) so bits 6 and 7 are both set, and we \ then need to flip bits 0-5, to convert the positive \ magnitude into its negative equivalent \ \ We can do this in one EOR instruction, as follows EOR #%01111111 \ Set bit 6 of A and flip bits 0-5, so A is now a \ negative number that we can add to the racing line in \ order to subtract the correct amount of steering ADC carRacingLine,X \ Steer the car right by the amount in A BCS mcar19 \ If the subtraction didn't underflow then we are still \ on the track, so jump to mcar19 to update the racing \ line with the new figure BCC mcar20 \ Otherwise the subtraction just underflowed, which \ means we just steered the car off the track, so jump \ to mcar20 so we don't update the car's racing line \ (this BCC is effectively a JMP as we just passed \ through a BCS) .mcar18 ADC carRacingLine,X \ Steer the car left by the amount in A BCS mcar20 \ If the addition just overflowed then this would steer \ the car off the track, so jump to mcar20 so we don't \ update the car's racing line (so we don't actually do \ the steer) .mcar19 STA carRacingLine,X \ Update the car's racing line with the updated figure, \ to steer the car across the track .mcar20 DEX \ Decrement the loop counter to point to the next driver BMI mcar21 \ If we have worked our way through all 20 drivers, jump \ to mcar21 to return from the subroutine CPX currentPlayer \ If driver X is the current player, jump up to mcar20 BEQ mcar20 \ to move on to the next driver JMP mcar1 \ Jump up to mcar1 to process the next driver .mcar21 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: BuildVisibleCar \ Type: Subroutine \ Category: 3D objects \ Summary: Check the distance to the specified car and build the car object \ if it is close enough \ Deep dive: Tactics of the non-player drivers \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The position of the driver whose distance we want to \ check \ \ ****************************************************************************** .BuildVisibleCar LDA driversInOrder,X \ Set A to the number of the driver in position X STA thisDriver \ Store the driver number in thisDriver so we can \ retrieve it later STA objectNumber \ Store the driver number in objectNumber, in case we \ need to hide this driver's car below TAX \ Set X to the driver number LDY #23 \ Set Y to 23, the object number we use to store the \ front segment of the track segment buffer SEC \ Set the C flag for a 16-bit calculation in the call \ to CompareSegments JSR CompareSegments \ Set A and T to the distance between driver X and \ the front segment in the track segment buffer in \ object Y BCS bvis1 \ If the C flag is set then the two cars are far apart, \ i.e. |T| < 128, so jump to bvis1 to hide the car EOR directionFacing \ This tests whether bit 7 of directionFacing and bit 7 BMI bvis1 \ of the distance in A are different, which will happen \ if either of the following is true: \ \ * We are facing forwards (0) and driver X is ahead \ of the front segment in the track segment buffer \ (1) \ \ * We are facing backwards (1) and driver X is not \ ahead of the front segment in the track segment \ buffer (0) \ \ In both cases driver X is too far away from us to be \ seen, and bit 7 of the result of the EOR will be set, \ so jump to bvis1 to hide the car LDA T \ Set T = |T| JSR Absolute8Bit \ STA T \ so A and T contain the absolute value of the distance \ between the car and the front segment in the track \ segment buffer CMP #40 \ If |A| < 40, jump to bvis2 to skip the following BCC bvis2 \ instruction and continue creating the car object \ If we get here then the car and the front segment in \ the track segment buffer are far apart, so we hide the \ car .bvis1 JMP HideObject \ Hide the object in objectNumber, which hides the car \ object for driver X, and return from the subroutine \ using a tail call .bvis2 \ If we get here, A and T contain the absolute value of \ the distance between the car and the front segment in \ the track segment buffer ASL A \ Set A = A * 2 \ = distance * 2 CLC \ Set A = ~(A + T) ADC T \ = ~(distance * 2 + distance) EOR #&FF \ = ~(distance * 3) SEC \ Set A = A + 1 + frontSegmentIndex ADC frontSegmentIndex \ = ~(distance * 3) + 1 + frontSegmentIndex \ = -(distance * 3) + frontSegmentIndex \ = frontSegmentIndex - distance * 3 \ \ frontSegmentIndex contains the index * 3 of the front \ segment in the track segment buffer, so this is the \ same as: \ \ (front segment index - distance) * 3 BPL bvis3 \ If the result was positive, i.e. track segment index \ > distance, jump to bvis3 to skip the following CLC \ Otherwise set A = A + 120 ADC #120 .bvis3 TAY \ Copy the result from A into Y, so Y now contains the \ track segment index * 3 of the track segment for the \ car object LDA carStatus,X \ If bit 4 of driver X's carStatus is set, then tactics AND #%00010000 \ are not enabled for this car, so jump to BNE BuildCarObjects \ BuildCarObjects to skip setting the car's steering LDA carSpeedHi,X \ If the high byte of driver X's speed is less than 50, CMP #50 \ jump to BuildCarObjects BCC BuildCarObjects LDA segmentSteering,Y \ Set the driver's carSteering to the segmentSteering STA carSteering,X \ value for this track segment, so the car follows the \ curve of the track \ ****************************************************************************** \ \ Name: BuildCarObjects (Part 1 of 3) \ Type: Subroutine \ Category: 3D objects \ Summary: Calculate the 3D coordinate of the specified car \ Deep dive: Drawing a 3D car from 2D parts \ \ ------------------------------------------------------------------------------ \ \ This routine calculates the 3D coordinate of the specified car, given its \ progress through the current segment and the racing line, as follows: \ \ [ xCoord2 ] [ xSegmentCoordI ] [ xTrackSegmentI ] \ [ yCoord2 ] = [ ySegmentCoordI mod 32 ] + [ yTrackSegmentI ] * carProgress \ [ zCoord2 ] [ zSegmentCoordI ] [ zTrackSegmentI ] \ \ [ xTrackSegmentO ] \ + [ 0 ] * carRacingLine * 4 \ [ zTrackSegmentO ] \ \ [ 0 ] \ + [ 144 ] \ [ 0 ] \ \ In the above: \ \ * xTrackSegmentI is the inner segment vector for the car's position \ \ * xTrackSegmentO is the outer segment vector for the car's position \ \ * xSegmentCoordI is the coordinate for the start of the car's track segment \ \ The routine then calculates the yaw and pitch angles for the car object (or \ objects). \ \ This part calculates the 3D coordinate of the car along the inside edge of \ the track, i.e. the first part of the above: \ \ [ xCoord2 ] [ xSegmentCoordI ] [ xTrackSegmentI ] \ [ yCoord2 ] = [ ySegmentCoordI mod 32 ] + [ yTrackSegmentI ] * carProgress \ [ zCoord2 ] [ zSegmentCoordI ] [ zTrackSegmentI ] \ \ The mod 32 part caps the y-coordinate to a maximum of 8192. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The driver number of the car object to build \ \ thisDriver Same as X \ \ Y The index * 3 of the track segment to use for the \ calculation \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ X X is set to the driver number in thisDriver \ \ xCoord2 Contains the object's 3D coordinates (for the one-object \ car) or the coordinates of the rear tyres (for the \ four-object car \ \ objYawAngle The object's yaw angle \ \ ****************************************************************************** .BuildCarObjects LDA segmentVector,Y \ Fetch the segment vector number for track segment Y, \ which gives us the segment vector number of the car \ object we want to build STA vectorNumber \ Store the segment vector number in vectorNumber so we \ can retrieve it in parts 2 and 3 STY T \ Store the index * 3 of the track segment in T TAY \ Set Y to the segment vector number of the car object LDA carProgress,X \ Set TT to the lowest byte of the car's progress STA TT \ through the current segment LDA carRacingLine,X \ Set UU to the car's current racing line STA UU LDA xTrackSegmentI,Y \ Set VV to the 3D x-coordinate of the inner segment STA VV \ vector for the car object LDA yTrackSegmentI,Y \ Set VV+1 to the 3D y-coordinate of the inner segment STA VV+1 \ vector for the car object LDA zTrackSegmentI,Y \ Set VV+2 to the 3D z-coordinate of the inner segment STA VV+2 \ vector for the car object \ We now calculate the following: \ \ xCoord2 = xSegmentCoordI \ + xTrackSegmentI * carProgress LDX #0 \ We are about to work our way through the three axes, \ so set X = 0 to use as an axis counter, working \ through the three axes x, y, z using X = 0, 1, 2 \ \ The comments below are for the x-axis LDA TT \ Set U = TT STA U \ = the lowest byte of the car's progress through \ the current segment LDY T \ Set Y to the index * 3 of the track segment that we \ stored above .bcar1 LDA #0 \ Set (V A) = 0 STA V LDA VV,X \ Set A to the x-coordinate of the inner segment vector \ We now calculate (V A T) = A * U, making sure we get \ the signs right BPL bcar2 \ If A is positive, jump to bcar2 to multiply A and U as \ they are \ If we get here then A is negative, so we need to apply \ the correct sign to the multiplication EOR #&FF \ Negate A (so it is now positive) CLC ADC #1 JSR Multiply8x8 \ Set (A T) = A * U EOR #&FF \ Negate A again (so it is now the correct sign for the CLC \ multiplication) ADC #1 BCS bcar3 \ If the addition just overflowed, then the result is \ now positive, which means V is already the correct \ high byte for (V A T), so jump to bcar3 DEC V \ Otherwise, decrement V to &FF so it's the correct \ high byte for (V A T) BCC bcar3 \ Jump to bcar3 (this BCC is effectively a JMP as we \ just passed through a BCS) .bcar2 JSR Multiply8x8 \ Set (A T) = A * U .bcar3 \ By this point, we have the following, signed result: \ \ (V A T) = A * U \ = xTrackSegmentI * carProgress \ \ We now add (V A) to the Y-th entry in xSegmentCoordI, \ which is the xSegmentCoordI entry for the track \ segment passed to the routine (Y contains the \ index * 3 of the track segment), and store the result \ in xCoord2 \ \ For the y-axis of the coordinate, i.e. for the \ multiplication: \ \ yCoord2 = ySegmentCoordI + (V A) \ \ then we add ySegmentCoordIHi mod 32 instead of \ ySegmentCoordIHi CLC \ Set (xCoord2Hi xCoord2Lo) ADC xSegmentCoordILo,Y \ = (xSegmentCoordIHi xSegmentCoordILo) + (V A) STA xCoord2Lo,X \ \ starting with the low bytes LDA xSegmentCoordIHi,Y \ And then the high bytes (though with a short interlude \ for when X = 1, when we add ySegmentCoordIHi mod 32 \ instead) PHP \ Store the C flag on the stack so we can retrieve it \ when adding the high bytes below CPX #1 \ If X = 1, set A = A mod 32 BNE bcar4 \ = ySegmentCoordIHi mod 32 AND #31 \ \ This caps the y-coordinate to a maximum of 8192 .bcar4 PLP \ Now we can finally add the high bytes ADC V STA xCoord2Hi,X INY \ Increment the axis pointer for xSegmentCoordI INX \ Increment the axis pointer for xCoord2 CPX #3 \ Loop back until X has looped through all three axes BNE bcar1 \ ****************************************************************************** \ \ Name: BuildCarObjects (Part 2 of 3) \ Type: Subroutine \ Category: 3D objects \ Summary: Add the racing line to the 3D coordinate of the specified car \ Deep dive: Drawing a 3D car from 2D parts \ \ ------------------------------------------------------------------------------ \ \ This part adds in the vector from the inside edge of the track to the car, \ i.e. the second part of the above: \ \ [ xCoord2 ] [ xCoord2 ] [ xTrackSegmentO ] \ [ yCoord2 ] = [ yCoord2 ] + [ 0 ] * carRacingLine * 4 \ [ zCoord2 ] [ zCoord2 ] [ zTrackSegmentO ] \ \ [ 0 ] \ + [ 144 ] \ [ 0 ] \ \ ****************************************************************************** \ We start by calculating the following: \ \ xCoord2 = xSegmentCoordI \ + xTrackSegmentO * carRacingLine * 4 \ \ for the x-axis and z-axis only LDY vectorNumber \ Set Y to the segment vector number that we stored \ above LDA xTrackSegmentO,Y \ Set VV to the 3D x-coordinate of the outer segment STA VV \ vector for the car object LDA zTrackSegmentO,Y \ Set VV+2 to the 3D x-coordinate of the outer segment STA VV+2 \ vector for the car object \ Note that VV+1 still contains the y-coordinate for the \ inner segment vector, which we can reuse as the height \ of the track from side-to-side is always the same, \ i.e. yTrackSegmentI = yTrackSegmentO for the same \ vector which means the track is always level along the \ y-axis LDX #0 \ We are about to work our way through the three axes, \ so set X = 0 to use as an axis counter, working \ through the three axes x, y, z using X = 0, 1, 2 \ \ The comments below are for the x-axis LDA UU \ Set U = UU STA U \ = the car's current racing line .bcar5 LDA #0 \ Set (V A) = 0 STA V LDA VV,X \ Set A to the x-coordinate of the outer segment vector \ We now calculate (V A T) = A * U, making sure we get \ the signs right BPL bcar6 \ If A is positive, jump to bcar6 to multiply A and U as \ they are \ If we get here then A is negative, so we need to apply \ the correct sign to the multiplication EOR #&FF \ Negate A (so it is now positive) CLC ADC #1 JSR Multiply8x8 \ Set (A T) = A * U EOR #&FF \ Negate A again (so it is now the correct sign for the CLC \ multiplication) ADC #1 BCS bcar7 \ If the addition just overflowed, then the result is \ now positive, which means V is already the correct \ high byte for (V A T), so jump to bcar7 DEC V \ Otherwise, decrement V to &FF so it's the correct \ high byte for (V A T) BCC bcar7 \ Jump to bcar7 (this BCC is effectively a JMP as we \ just passed through a BCS) .bcar6 JSR Multiply8x8 \ Set (A T) = A * U .bcar7 \ By this point, we have the following, signed result: \ \ (V A T) = A * U \ = xTrackSegmentO * carRacingLine ASL A \ Set (V A) = (V A) * 4 ROL V \ = xTrackSegmentO * carRacingLine * 4 ASL A ROL V CLC \ Set (xCoord2Hi xCoord2Lo) = (xCoord2Hi xCoord2Lo) ADC xCoord2Lo,X \ + (V A) STA xCoord2Lo,X \ \ starting with the low bytes LDA xCoord2Hi,X \ And then the high bytes ADC V STA xCoord2Hi,X INX \ Set X = X + 2, so we skip the y-axis INX CPX #4 \ Loop back to bcar5 until we have processed the x-axis BNE bcar5 \ (for X = 0) and z-axis (for X = 2) \ Finally, we add 144 to the y-coordinate LDA yCoord2Lo \ Set (yCoord2Hi yCoord2Lo) += 144 CLC \ ADC #144 \ starting with the low bytes STA yCoord2Lo BCC bcar8 \ And then the high bytes INC yCoord2Hi \ ****************************************************************************** \ \ Name: BuildCarObjects (Part 3 of 3) \ Type: Subroutine \ Category: 3D objects \ Summary: Calculate the screen coordinates of all the objects in the \ specified car \ Deep dive: Drawing a 3D car from 2D parts \ \ ------------------------------------------------------------------------------ \ \ Now that we have the car's 3D coordinates in xCoord2, we calculate the car's \ yaw and pitch angles, and use them to create either one car object, or four \ car objects if this is the four-object car. \ \ If this is the four-object car (i.e. the car is directly in front of us, is \ close enough and is visible), then we calculate the coordinates for the three \ extra objects as follows: \ \ [ xCoord2 ] [ xTrackSegmentI ] \ Front tyres = [ yCoord2 ] + [ yTrackSegmentI ] / 2 \ [ zCoord2 ] [ zTrackSegmentI ] \ \ [ xCoord2 ] [ xTrackSegmentI ] \ Body and helmet = [ yCoord2 ] + [ yTrackSegmentI ] / 4 \ [ zCoord2 ] [ zTrackSegmentI ] \ \ [ xCoord2 ] [ xTrackSegmentI ] \ Rear tyres = [ yCoord2 ] + [ yTrackSegmentI ] / 8 \ [ zCoord2 ] [ zTrackSegmentI ] \ \ ****************************************************************************** .bcar8 LDA #4 \ Set A = 4, to use as the object type for the car (we \ start with the object type of the standard car, and \ change this later if required) JSR GetObjectAngles-2 \ Calculate the object's yaw and pitch angles, using the \ coordinates in xCoord2, and set the object's \ visibility, scale and type LDX thisDriver \ Set X = thisDriver (driver number of car we are \ driving) LDA objectDistanceHi \ Set A to the high byte of the distance of the object CMP #3 \ If A >= 3, then the car is not close enough to be the BCS bcar11 \ four-object car, so jump to bcar11 to check whether it \ should be built as a distant car object \ If we get here then A <= 2, so the car is close enough \ to consider building as a four-object car LDA thisPosition \ If the car we are building is not the car just ahead CMP positionAhead \ of us in the race, then it can't be the four-object BNE bcar10 \ car, so jump to bcar10 to return from the subroutine \ with the car built as a standard car LDA objectStatus,X \ If bit 7 of the car's object status byte is set, then BMI bcar9 \ the car is not visible, so jump to bcar9 to skip the \ following instruction (which leaves the car object as \ a standard car, but still builds the other three car \ objects) \ If we get here then the car we are building is the \ nearest car in front of us, it's close and it is \ visible, so we draw this car as the four-object car DEC objectStatus,X \ The object type is stored in bits 0-3 of objectStatus, \ so this decrements the car's object type from 4 (the \ standard car) to 3 (the rear wing in the four-object \ car) .bcar9 LDY vectorNumber \ Set Y to the segment vector number that we stored \ above JSR GetSegmentVector \ Fetch the inner segment vector for the part of the \ track that the car is on: \ \ [ (SS T) ] [ xTrackSegmentI ] \ [ (TT U) ] = [ yTrackSegmentI ] \ [ (UU V) ] [ yTrackSegmentI ] \ \ So this contains the direction of the track where the \ car is JSR HalveCoordinate \ Halve the coordinate in (SS T), (TT U) and (UU V) LDY #&FD \ Set Y = &FD so the call to AddVectors uses xCoord2 LDX #&FA \ Set X = &FA so the call to AddVectors uses xCoord1 JSR AddVectors \ Set: \ \ [ (SS T) ] \ xCoord1 = xCoord2 + [ (TT U) ] / 2 \ [ (UU V) ] \ \ So xCoord1 contains the 3D coordinates of the front \ tyres of the four-object car JSR HalveCoordinate \ Halve the coordinate in (SS T), (TT U) and (UU V) LDX #&F4 \ Set X = &F4 so the call to AddVectors uses \ xHelmetCoord JSR AddVectors \ Set: \ \ [ (SS T) ] \ xHelmetCoord = xCoord2 + [ (TT U) ] / 4 \ [ (UU V) ] \ \ So xHelmetCoord contains the 3D coordinates of the \ helmet and body of the four-object car JSR HalveCoordinate \ Halve the coordinate in (SS T), (TT U) and (UU V) LDX #&FD \ Set X = &FD so the call to AddVectors uses xCoord2 JSR AddVectors \ Set: \ \ [ (SS T) ] \ xCoord2 = xCoord2 + [ (TT U) ] / 8 \ [ (UU V) ] \ \ So xCoord2 contains the 3D coordinates of the rear \ tyres of the four-object car \ Now that we have the 3D coordinates of the extra three \ parts of the four-object car, we can calculate the \ object's yaw and pitch angles, and store the details \ in objects 20, 21 and 22 (for the rear tyres, \ body/helmet and front tyres respectively) LDA #20 \ Set objectNumber = 20, to use as then object number STA objectNumber \ for the rear tyres in the four-object car LDA #2 \ Set A = 2, to use as the object type for the rear \ tyres in the four-object car JSR GetObjectAngles-2 \ Calculate the object's yaw and pitch angles, using the \ coordinates of the rear tyres in xCoord2, and set the \ object's visibility, scale and type LDA #21 \ Set objectNumber = 21, to use as then object number STA objectNumber \ for the body and helmet in the four-object car LDA #1 \ Set A = 1, to use as the object type for the body and \ helmet in the four-object car LDX #&F4 \ Set X = &F4 so the call to GetObjectAngles uses \ xHelmetCoord JSR GetObjectAngles \ Calculate the object's yaw and pitch angles, using the \ coordinates of the body and helmet in xHelmetCoord, \ and set the object's visibility, scale and type LDA #22 \ Set objectNumber = 22, to use as then object number STA objectNumber \ for the front tyres in the four-object car LDA #0 \ Set A = 0, to use as the object type for the rear \ tyres in the four-object car LDX #&FA \ Set X = &FA so the call to GetObjectAngles uses \ xCoord1 JSR GetObjectAngles \ Calculate the object's yaw and pitch angles, using the \ coordinates of the front tyres using xCoord1, and set \ the object's visibility, scale and type .bcar10 LDX thisDriver \ Set X to the driver number that we stored at the start \ of the BuildVisibleCar routine RTS \ Return from the subroutine .bcar11 \ We jump here when objectDistanceHi >= 3, so we now \ need to check whether the car is far enough away for \ us to change it to a distant car object \ \ We jump here with A set to objectDistanceHi CMP #5 \ If A < 5, i.e. objectDistanceHi = 4, then the car is BCC bcar10 \ close enough to stay as a standard car object, so jump \ to bcar10 to return from the subroutine LDA objectStatus,X \ If bit 7 of the car's object status byte is set, then BMI bcar10 \ the car is not visible, so jump to bcar10 to return \ from the subroutine \ If we get here then objectDistanceHi >= 5 and the car \ is visible, so the car is far enough away to be a \ distant car object INC objectStatus,X \ The object type is stored in bits 0-3 of objectStatus, \ so this increments the car's object type from 4 (the \ standard car) to 5 (the distant car) RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: GetObjectAngles \ Type: Subroutine \ Category: 3D objects \ Summary: Calculate the object's yaw and pitch angles, and set the object's \ visibility, scale and type \ Deep dive: Pitch and yaw angles \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ A Object type \ \ X The offset of the variable to use for the object's 3D \ coordinates in the GetObjYawAngle routine: \ \ * &F4 = xHelmetCoord \ \ * &FA = xCoord1 \ \ * &FD = xCoord2 \ \ ------------------------------------------------------------------------------ \ \ Other entry points: \ \ GetObjectAngles-2 Use xCoord2 for the object's 3D coordinates in the call \ to GetObjYawAngle \ \ ****************************************************************************** LDX #&FD \ Set X = &FD so the calls to GetObjYawAngle and \ GetObjPitchAngle use xCoord2 and yCoord2 for the \ object's 3D coordinates .GetObjectAngles STA objectType \ Store the object type in objectType JSR GetObjYawAngle-2 \ Calculate the object's yaw angle, from the point of \ view of the player, returning it in (JJ II) LDY objectNumber \ Set Y to the number of the object we are processing LDA II \ Set the yaw angle for this object in (objYawAngleHi STA objYawAngleLo,Y \ objYawAngleLo) to (JJ II) LDA JJ STA objYawAngleHi,Y JSR CheckForContact-2 \ Check to see if the object and the player's car are \ close enough for contact, specifically if they are \ within a distance of 37 from each other JSR GetObjPitchAngle-2 \ Calculate the object's pitch angle, from the point \ of view of the player, 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 \ routine \ Fall through into SetObjectDetails to set the object's \ visibility, scale and type \ ****************************************************************************** \ \ Name: SetObjectDetails \ Type: Subroutine \ Category: 3D objects \ Summary: Set an object's visibility, scale and type \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ A The object's pitch angle, as returned by \ GetObjPitchAngle \ \ C flag If the C flag is set, hide the object \ \ scaleUp The scale up factor, as returned by GetObjPitchAngle \ \ scaleDown The scale down factor, as returned by GetObjPitchAngle \ \ ****************************************************************************** .SetObjectDetails LDY objectNumber \ Set Y to the number of the object BCS HideObject \ If the C flag is set, jump to HideObject to hide the \ object and return from the subroutine using a tail \ call SEC \ Set A = A - 1 SBC #1 BMI HideObject \ If the result is negative, jump to HideObject to hide \ the object and return from the subroutine using a tail \ call STA objectPitchAngle,Y \ Store the object's pitch angle in objectPitchAngle \ We now set the object's scaleUp value (i.e. its size) \ to the following: \ \ scaleUp / 2 ^ (scaleDown - 10) \ \ where scaleUp and scaleDown were set by the call to \ GetObjPitchAngle LDA scaleDown \ Set X = scaleDown - 9 SEC SBC #9 TAX LDA scaleUp \ Set A = scaleUp DEX \ Set X = X - 1 \ = scaleDown - 10 BEQ sobj3 \ If X = 0, jump to sobj3 to set the object's scaleUp \ to A BPL sobj2 \ If X > 0, jump to sobj3 to set the object's scaleUp \ to A << X \ Otherwise X < 0, so we calculate A >> X to set as the \ object's scaleUp .sobj1 LSR A \ Set A = A >> 1 INX \ Increment the shift counter in X BNE sobj1 \ Loop back to sobj1 until we have shifted by X places BEQ sobj3 \ Jump to sobj3 (this BEQ is effectively a JMP as we \ just passed through a BNE) .sobj2 ASL A \ Set A = A << 1 DEX \ Decrement the shift counter in X BNE sobj2 \ Loop back to sobj2 until we have shifted by X places .sobj3 STA objectSize,Y \ Set the object's size to the scaled value of scaleUp LDA objectStatus,Y \ Set A to the object's status byte AND #%01110000 \ Clear bit 7 to make the object visible, and set bits ORA objectType \ 0-3 to the object type JMP SetObjectStatus \ Jump to SetObjectStatus to store the updated object \ status byte, returning from the subroutine using a \ tail call \ ****************************************************************************** \ \ Name: HideObject \ Type: Subroutine \ Category: 3D objects \ Summary: Set an object to be hidden \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ objectNumber The number of the object to hide \ \ ****************************************************************************** .HideObject LDY objectNumber \ Set Y to the number of the object to hide LDA objectStatus,Y \ Set A to the object's status byte ORA #%10000000 \ Set bit 7 in the object's status byte in A, which \ marks the object as hidden \ Fall through into SetObjectStatus to store the updated \ object status byte \ ****************************************************************************** \ \ Name: SetObjectStatus \ Type: Subroutine \ Category: 3D objects \ Summary: Set an object's status byte \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ A The object status byte \ \ Y The number of the object \ \ ****************************************************************************** .SetObjectStatus STA objectStatus,Y \ Set object Y's status byte to the value in A RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: CheckForContact \ Type: Subroutine \ Category: Car geometry \ Summary: Check to see if the object is close enough to the player car to \ make contact \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ Y Contact is made if objectDistanceLo <= Y \ \ (J I) max(|x-delta|, |z-delta|) \ \ (H G) min(|x-delta|, |z-delta|) \ \ M The smaller yaw angle of the object, where 0 to 255 \ represents 0 to 45 degrees \ \ objectNumber The number of the object being checked for contact \ \ ------------------------------------------------------------------------------ \ \ Other entry points: \ \ CheckForContact-2 Set Y = 37, so we make contact if (L K) <= 37 \ \ ****************************************************************************** LDY #37 \ Set Y = 37, so we make contact if (L K) <= 37 .CheckForContact JSR GetObjectDistance \ Set (L K) to the distance between the object and the \ player's car LDA L \ Set objectDistanceHi to the high byte of (L K) STA objectDistanceHi BNE ccon1 \ If objectDistanceHi is non-zero then the objects are \ too far apart for a collision, so jump to ccon1 to \ return from the subroutine CPY K \ If K > Y, then the objects are too far apart for a BCC ccon1 \ collision, this time in terms of the low byte of the \ distance, so jump to ccon1 to return from the \ subroutine \ If we get here then K <= Y, so the objects are close \ enough for a collision DEC processContact \ Decrement processContact so it is non-zero, so we \ check for contact between this car and the player's \ car in the ProcessContact routine LDA K \ Set (objectDistanceHi objectDistanceLo) = (L K) STA objectDistanceLo LDA objectNumber \ Store the number of the other object in STA collisionDriver \ collisionDriver, so we know who's crashing .ccon1 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: DrawCarInPosition \ Type: Subroutine \ Category: Drawing objects \ Summary: Draw the car in a specified race position \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The race position of the car to draw \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ X X is preserved \ \ ****************************************************************************** .DrawCarInPosition STX xStoreDraw \ Store X in xStoreDraw so it can be retrieved at the \ end of the DrawCarOrSign routine LDA driversInOrder,X \ Set X to the number of the driver in position X TAX \ Fall through into DrawCarOrSign to draw the car whose \ driver number we just looked up, i.e. the car in \ position X \ ****************************************************************************** \ \ Name: DrawCarOrSign \ Type: Subroutine \ Category: Drawing objects \ Summary: Draw a car or sign \ Deep dive: Object definitions \ Drawing a 3D car from 2D parts \ Road signs \ \ ------------------------------------------------------------------------------ \ \ This routine is used to draw road signs and cars. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The car or sign to draw: \ \ * 0-19 = Draw the car for this driver number \ \ * 20-22 = Draw one of the three extra objects that \ make up the four-object car: \ \ * 20 = rear wing \ \ * 21 = body and helmet \ \ * 22 = front tyres \ \ * 23 = Draw the road sign \ \ xStoreDraw The value to restore into X at the end of the routine \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ X X is set to xStoreDraw \ \ ****************************************************************************** .DrawCarOrSign LDA objectStatus,X \ Set A to this object's status byte BMI dcas3 \ If bit 7 is set then the object is not visible, so \ jump to dcas3 to return from the subroutine without \ drawing anything AND #%00001111 \ Extract the object's object type from bits 0-3 and STA objectType \ store it in objectType LDA objYawAngleLo,X \ Set (A T) = objYawAngle for this object SEC \ - playerYawAngle SBC playerYawAngleLo \ STA T \ starting with the low bytes LDA objYawAngleHi,X \ And then the high bytes SBC playerYawAngleHi \ \ So (A T) now contains the amount that the object we \ are drawing is to the left or right of the player's \ car BPL dcas1 \ If the result is positive, jump to dcas1 to perform a \ positive comparison CMP #&E0 \ The result is negative, so check to see if A < -32, BCC dcas3 \ and if so, jump to dcas3 to return from the subroutine \ without drawing anything BCS dcas2 \ Jump to dcas2 (this BCS is effectively a JMP as we \ just passed through a BCC) .dcas1 CMP #32 \ The result is positive, so check to see if A >= 32, BCS dcas3 \ and if so, jump to dcas3 to return from the subroutine \ without drawing anything .dcas2 \ If we get here then -32 <= A < 32 ASL T \ Set (A T) = (A T) * 4 ROL A \ ASL T \ so -128 <= A < 128 ROL A CLC \ Set xPixelCoord = 80 + A ADC #80 \ STA xPixelCoord \ This moves xPixelCoord so that it is centred on the \ screen, as the centre x-coordinate of the screen is \ at 80 pixels LDA objectPitchAngle,X \ Set yPixelCoord to this object's pitch angle STA yPixelCoord LDA objectSize,X \ Set scaleUp to this object's size (i.e. the size of STA scaleUp \ the car) JSR DrawObject \ Draw the object on-screen .dcas3 LDX xStoreDraw \ Set X = xStoreDraw so X is unchanged by the routine \ call RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: HalveCoordinate \ Type: Subroutine \ Category: Maths (Geometry) \ Summary: Halve a coordinate with three 16-bit axes \ \ ------------------------------------------------------------------------------ \ \ Given a three-axis variable, this routine halves each axis in-place: \ \ [ (SS T) ] [ (SS T) ] \ [ (TT U) ] = [ (TT U) ] / 2 \ [ (UU V) ] [ (UU V) ] \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ (SS T) The value of the coordinate's first axis \ \ (TT U) The value of the coordinate's second axis \ \ (UU V) The value of the coordinate's third axis \ \ ****************************************************************************** .HalveCoordinate LDX #2 \ We are about to right-shift the following 16-bit \ variables: \ \ (SS T) \ (TT U) \ (UU V) \ \ so set a counter in X to use as an index that loops \ through 2, 1 and 0, as: \ \ (TT U) = (SS+1 U+1) \ (UU V) = (SS+2 U+2) \ \ The following comments are for (SS T), but the same \ process applies for (TT U) and (UU V) .halc1 LDA SS,X \ Set A to the high byte of (SS T) CLC \ If A is negative, set the C flag, otherwise clear the BPL halc2 \ C flag, so this sets the C flag to the sign of (SS T) SEC .halc2 ROR SS,X \ Set (SS T) = (SS T) >> 1 ROR T,X DEX \ Decrement the loop counter to move on to the next \ variable BPL halc1 \ Loop back until we have shifted all three 16-bit \ variables to the right RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: vergeTableHi \ Type: Variable \ Category: Drawing the track \ Summary: High bytes of the addresses of the four verge tables \ \ ****************************************************************************** .vergeTableHi EQUB HI(leftVergeStart) EQUB HI(leftTrackStart) EQUB HI(rightVergeStart) EQUB HI(rightGrassStart) \ ****************************************************************************** \ \ Name: vergeTableLo \ Type: Variable \ Category: Drawing the track \ Summary: Low bytes of the addresses of the four verge tables \ \ ****************************************************************************** .vergeTableLo EQUB LO(leftVergeStart) EQUB LO(leftTrackStart) EQUB LO(rightVergeStart) EQUB LO(rightGrassStart) \ ****************************************************************************** \ \ Name: DrawSegmentEdge (Part 1 of 7) \ Type: Subroutine \ Category: Drawing the track \ Summary: Draw a single segment's edge as part of a whole track verge edge \ Deep dive: Drawing the track verges \ \ ------------------------------------------------------------------------------ \ \ This part of the routine checks whether the edge is on-screen, off-screen or \ partially on-screen, and stores the yaw and pitch angles of the edge in W and \ RR respectively. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ A Index into the vergePixelMask table for the pixel bytes \ that make up the colours of this edge in this segment \ \ X Index in the verge buffer of the track segment list \ entry for the verge, pointing to either the inner edge \ or outer edge of the verge mark, depending on the verge \ table we are drawing (so we can use this for fetching \ yaw angles from the track segment list for the verge) \ \ Y Index in the verge buffer of the track segment list \ entry for the verge, for the inner edge of the verge \ mark (so we can use this for fetching pitch angles from \ the track segment list for the verge) \ \ C flag Flag to control whether we draw the edge, or just set up \ variables for the next call: \ \ * Clear = draw this edge \ \ * Set = do not draw this edge, but do set up the \ variables to pass to the next call \ \ M Yaw angle from the previous call to DrawSegmentEdge \ \ N Pitch angle from the previous call to DrawSegmentEdge \ \ prevYawIndex Same as X for the first call, or the yaw angle index of \ the previous call (i.e. the previous segment) if this is \ not the first call \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ X X is unchanged \ \ Y Y is unchanged \ \ M Set to 128 + edge's yaw angle * 4, to carry through to \ the next call to DrawSegmentEdge \ \ N Set to the edge's pitch angle to carry through to the \ next call to DrawSegmentEdge \ \ ****************************************************************************** .DrawSegmentEdge PHP \ Store the C flag on the stack so we can retrieve it \ later STA pixelMaskIndex \ Store the index into the vergePixelMask table in \ pixelMaskIndex so we can use it later LDA #0 \ Set vergeOnScreenEdge = 0 to denote that the edge is STA vergeOnScreenEdge \ fully on-screen or fully off-screen (we update this \ below if it turns out not to be the case) LDA yVergeRight,Y \ Set A to the pitch angle - 1 of the verge edge we are SEC \ drawing SBC #1 CMP #78 \ If A >= 78, jump to dver2 with the C flag set to BCS dver2 \ indicate that this verge edge is off-screen LDA xVergeRightHi,X \ Set A to the high byte of the yaw angle of the verge \ edge we are drawing BPL dver1 \ If A is positive, jump to dver1 to skip the following EOR #&FF \ Set A = ~A \ = -A - 1 \ = |A| - 1 \ \ So A is now positive is approximately |A| .dver1 CMP #20 \ If A >= 20, set the C flag, otherwise clear the C flag \ \ Because the field of view is 20 degrees, the C flag is \ now set if the verge edge is off-screen, or clear if \ it is on-screen .dver2 ROR GG \ Rotate the C flag into bit 7 of RR, so bit 7 is set if \ the edge is off-screen, or clear if it is on-screen \ \ If this is not the first call to DrawSegmentEdge, then \ bit 6 will now contain the on-screen/off-screen bit \ for the previous segment's verge edge LDA xVergeRightHi,X \ Set (W A) to the yaw angle of the verge edge we are STA W \ drawing LDA xVergeRightLo,X ASL A \ Set (W A) = (W A) * 4 ROL W \ ASL A \ So W is the high byte of the yaw angle * 4 ROL W LDA W \ Set W = W + 128 CLC \ ADC #128 \ So W contains 128 + yaw angle * 4 (if we drop the low STA W \ byte, which is the fractional part) \ \ Adding 128 moves the angle range from -128 to +128, to \ 0 to 255, so when we calculate the difference between \ the yaw angles for two segments by subtracting them \ below, the C flag will be set correctly LDA yVergeRight,Y \ Set RR to the pitch angle of the verge edge we are STA RR \ drawing STX thisYawIndex \ Store the index of the yaw angle in the track segment \ list for this verge in thisYawIndex so we can use it \ later STY thisPitchIndex \ Store the index of the pitch angle in the track \ segment list for this verge in thisPitchIndex so we \ can use it later PLP \ Retrieve the C flag that we passed to the routine and \ stored on the stack earlier BCS dver8 \ If the C flag is set then we don't draw this edge, so \ jump to dver28 via dver8 to clean up and return from \ the subroutine BIT GG \ If bit 6 of GG is clear, then the previous segment's BVC dver3 \ verge edge from the last call to DrawSegmentEdge was \ on-screen, so jump to dver3 \ If we get here then previous segment's verge edge from \ the last call to DrawSegmentEdge was off-screen BMI dver8 \ If bit 7 of GG is set, then this segment's verge edge, \ which we are now drawing, is also off-screen, so jump \ to dver28 via dver8 to clean up and return from the \ subroutine \ If we get here then the previous verge edge was \ off-screen but this one is on-screen \ We now swap the values of M and W, and the values of \ N and RR, which sets W and RR to the yaw and pitch \ angles of the previous edge LDX M \ Set X = M and Y = N LDY N LDA W \ Set M = W STA M LDA RR \ Set N = RR STA N STX W \ Set W = X \ = M STY RR \ Set RR = Y \ = N DEC vergeOnScreenEdge \ Set vergeOnScreenEdge = &FF (as it was set to 0 above) \ to flag that this edge is partially off-screen and \ that we have swapped the angles over \ By this point, we have the following: \ \ * W = the yaw angle of the edge to draw \ \ * RR = the pitch angle of the edge to draw \ \ * M = the yaw angle of the previous edge \ \ * N = the pitch angle of the previous edge \ \ We only get to this point if this is not the first \ call to DrawSegmentEdge for this verge edge, so we \ know that M and N are defined \ ****************************************************************************** \ \ Name: DrawSegmentEdge (Part 2 of 7) \ Type: Subroutine \ Category: Drawing the track \ Summary: Set up the edge's gradient \ Deep dive: Drawing the track verges \ \ ------------------------------------------------------------------------------ \ \ This part calculates the edge's gradient in terms of the change in yaw and \ pitch angles, and stores them in SS and TT, with SS containing the yaw delta, \ and TT containing the pitch delta. \ \ ****************************************************************************** .dver3 LDA RR \ Set A = RR - N SEC SBC N STA WW \ Set WW = A \ \ So WW contains the current edge's pitch angle minus \ the previous edge's pitch angle, so: \ \ * WW is positive if the current pitch angle is \ greater than the previous pitch angle, so the \ current edge is higher up the screen, so the edge \ is moving up the screen \ \ * WW is negative if the current pitch angle is \ less than the previous pitch angle, so the \ current edge is lower down the screen, so the edge \ is moving down the screen BPL dver4 \ If A is positive, jump to dver4 LDA #0 \ Set A = -WW SEC \ = -A SBC WW \ \ So A is now positive, i.e. A = |RR - N| .dver4 STA TT \ Set TT = A \ = |RR - N| \ \ So TT contains the difference in pitch angle between \ the previous edge and the current edge, so let's call \ it dPitch: \ \ TT = |dPitch| LDA GG \ If bits 6 and 7 of GG are clear, then both this and AND #%11000000 \ the previous segment's verge edges were on-screen, so BEQ dver9 \ jump to dver9 to calculate the difference in yaw \ angles in SS \ If we get here then one of the previous segment's \ verge edges were off-screen, so we need to interpolate \ the angles to calculate where the edge meets the \ screen edge LDY thisYawIndex \ Set Y to the index of the yaw angle in the track \ segment list for this verge LDX prevYawIndex \ Set X to the index of the yaw angle in the track \ segment list for the segment from the previous call LDA xVergeRightLo,Y \ Set (VV T) to the difference in yaw angle between the SEC \ current segment and the previous segment, starting SBC xVergeRightLo,X \ with the low bytes STA T LDA xVergeRightHi,Y \ And then the high bytes SBC xVergeRightHi,X STA VV \ This also sets the sign of VV as follows: \ \ * VV is positive if the previous yaw angle is less \ than the current yaw angle, so the edge is moving \ from left to right \ \ * VV is negative if the previous yaw angle is \ greater than the current yaw angle, so the edge is \ moving from right to left JSR Absolute16Bit \ Set (A T) = |A T| \ \ So (A T) contains the difference in yaw angle between \ the previous edge and the current edge, so let's call \ it dYaw: \ \ (A T) = |dYaw| \ We now scale (A T) and TT, scaling (A T) up while \ scaling TT down, up to a maximum of two shifts up and \ down, as follows: \ \ * If |dYaw| >= 64, |dPitch| / 4 \ \ * If |dYaw| >= 32, |dYaw| * 2, |dPitch| / 2 \ \ * If |dYaw| < 16, |dYaw| * 4 \ \ This scales |dYaw| to be as large as possible while \ simultaneously making |dPitch| as small as possible CMP #64 \ If A >= 64, jump to dver5 to divide TT by 4 BCS dver5 ASL T \ Set (A T) = (A T) * 2 ROL A \ = |dYaw| * 2 CMP #64 \ If A >= 64, jump to dver6 to divide TT by 2 BCS dver6 ASL T \ Set (A T) = (A T) * 2 ROL A \ = |dYaw| * 4 BPL dver7 \ Jump to dver7 (this BPL is effectively a JMP as bit 7 \ of A will never be set, as the maximum value of A \ before the last ROR was 63, and 63 << 1 = 126, which \ has bit 7 clear) .dver5 LSR TT \ Set TT = TT / 2 \ = |dPitch| / 2 .dver6 LSR TT \ Set TT = TT / 2 \ = |dPitch| / 2 .dver7 STA SS \ Set SS = A \ = scaled |dYaw| LDA VV \ If vergeOnScreenEdge = &FF, then this sets VV to ~VV, EOR vergeOnScreenEdge \ which flips bit 7 (which we will use later) STA VV LDA SS \ Set A = SS JMP dver10 \ Jump to dver10 to finish setting up SS and TT .dver8 JMP dver28 \ Jump to dver28 (this is used as a way for branch \ instructions to jump to dver28, when it is too far \ for a branch instruction to reach .dver9 \ If we get here then both the previous and current \ verge edges are on-screen, so we need to set SS to the \ difference in yaw angle between the previous and \ current edges \ \ Note that we added 128 to each yaw angle to convert \ them in the range 0 to 255 (rather than -128 to +127) \ so the subtraction will set the C flag correctly LDA M \ Set A = M - W SEC \ SBC W \ So A contains the difference in yaw angle between the \ previous edge and the current edge, so let's call it \ dYaw ROR VV \ Rotate the C flag into bit 7 of VV, so bit 7 is set if \ M >= W, or clear if M < W BMI dver10 \ If A is negative, jump to dver10 to finish setting up \ SS and TT EOR #&FF \ Negate A using two's complement, so A is negative, CLC \ i.e. A = -|A| ADC #1 \ We now fall through into dver10 to set SS to A, which \ contains -|M - W|, or -|dYaw| .dver10 STA SS \ Set SS = A BNE dver11 \ If A is non-zero, jump to dver11 ORA TT \ A is zero, so this sets A to TT BEQ dver8 \ If A is zero (which means both SS and TT are zero), \ jump to dver28 via dver8 to clean up and return from \ the subroutine \ By this point, we have the following: \ \ * SS = the scaled yaw delta along the edge to draw \ (scaled to be as large as possible) \ \ * TT = the scaled pitch delta along the edge to draw \ (scaled to be as small as possible) \ \ * VV = the left-right direction of the edge \ \ * Positive = left to right \ \ * Negative = right to left \ \ * WW = the up-down direction of the edge \ \ * Positive = line is heading up the screen \ \ * Negative = line is heading down the screen \ \ So SS and TT are set to the gradient of the edge that \ we are drawing, and WW and VV are set to the direction \ of the edge \ ****************************************************************************** \ \ Name: DrawSegmentEdge (Part 3 of 7) \ Type: Subroutine \ Category: Drawing the track \ Summary: Modify the drawing routines according to the edge gradient \ Deep dive: Drawing the track verges \ \ ------------------------------------------------------------------------------ \ \ This part modifies the edge-drawing routines for the verge so they move in the \ right directions depending on the signs of the deltas calculated previously, \ and set up pixel bytes and masks for the correct colour scheme depending on \ which verge edge is being drawn. \ \ ****************************************************************************** .dver11 LDA GG \ Set A to bits 6 and 7 of GG AND #%11000000 BEQ dver12 \ If bits 6 and 7 of GG are clear, then both this \ segment's verge edge and the previous segment's verge \ edge were on-screen, so jump to dver12 \ If we get here then either this segment's verge edge \ or the previous segment's verge edge was off-screen \ \ We also set bit 7 of VV above to the following: \ \ * Bit 7 is clear if this yaw angle >= previous yaw \ angle \ \ * Bit 7 is set if this yaw angle < previous yaw \ angle \ \ with these flipped if vergeOnScreenEdge is &FF, which \ means the edge is partially on-screen \ \ So bit 7 is set if: \ \ * The edge is not partially on-screen and this yaw \ angle < previous yaw angle, in which case we have \ just moved left, falling off the left edge of the \ screen \ \ * The edge is partially on-screen and this yaw \ angle >= previous yaw angle, in which case we have \ just moved right onto the screen from the left \ edge \ \ In either case we are drawing over the left edge of \ the screen, so we need will need to update the \ background colour for this track line LDA VV \ Set A to bit 7 of VV to store in updateBackground, so AND #%10000000 \ A is non-zero if we are drawing on the left edge of \ the screen .dver12 STA updateBackground \ Store A in updateBackground, so we update the \ background colour table in the StopDrawingEdge routine \ We set WW above to the previous edge's pitch angle \ minus this edge's pitch angle, so it is positive if \ the edge is heading up the screen, negative if it is \ heading down the screen LDA WW \ If WW is non-zero, jump to dver13 BNE dver13 \ If we get here then this edge and the previous edge \ have the same pitch angle LDA vergeOnScreenEdge \ Set WW = ~vergeOnScreenEdge EOR #&FF \ STA WW \ So WW is 0 if the edge is partially on-screen, or &FF \ otherwise .dver13 \ We now modify both the DrawVergeByteLeft and \ DrawVergeByteRight routines so they do nothing on \ entry, and either increment or decrement Y on exit, \ depending on the polarity of WW \ \ We modify the routines as follows: \ \ * If WW is positive, so the edge is heading up the \ screen, then do NOP on entry and INY on exit, so \ we increase the track line as we draw the edge \ \ * If WW is negative, so the edge is heading down the \ screen, then do NOP on entry and DEY on exit, so \ we decrease the track line as we draw the edge \ \ This ensures that when we call these routines, they \ increase or decrease the track line in the appropriate \ direction for the line we are drawing BPL dver14 \ If WW is positive, jump to dver14 LDA #&88 \ Set A to the opcode for the DEY instruction BNE dver15 \ Jump to dver15 (this BNE is effectively a JMP as A is \ never zero) .dver14 LDA #&C8 \ Set A to the opcode for the INY instruction .dver15 STA verl5 \ Modify the instructions at verl5 and verb5 as follows: STA verb5 \ \ * INY when WW is positive \ \ * DEY when WW is negative \ \ So the track line in Y increases when WW is positive \ and the line is heading up the screen LDA #&EA \ Set A to the opcode for the NOP instruction STA verl1 \ Modify the instruction at verl1 to NOP, so the \ DrawVergeByteLeft routine doesn't update Y on entry STA verb1 \ Modify the instruction at verb1 to NOP, so the \ DrawVergeByteRight routine doesn't update Y on entry LDY pixelMaskIndex \ Set Y = pixelMaskIndex so we can use it to fetch the \ correct pixel bytes from vergePixelMask for this edge LDX #0 \ We are about to populate four bytes in objectPalette \ and vergeEdgeRight, so set X as a loop counter that \ starts at 0 \ \ We populate objectPalette with the four bytes at \ offset Y in vergePixelMask, and vergeEdgeRight with \ the same bytes, but masked to only include the \ rightmost 4, 3, 2 and 1 pixels \ \ This means the objectPalette table actually contains \ pixel bytes at this point, rather than full colour \ four-pixel bytes as it does in the rest of the code, \ but we can still think of it as containing the \ "palette" for the track verges, it's just a more \ sophisticated palette - stripey toothpaste is still \ toothpaste, after all .dver16 LDA vergePixelMask,Y \ Set A to the Y-th verge pixel mask in vergePixelMask STA objectPalette,X \ Store the full pixel mask in objectPalette AND pixelsEdgeRight,X \ Store the pixel mask in vergeEdgeRight, with all the STA vergeEdgeRight,X \ pixels to the right of position X set, plus pixel X INY \ Increment Y so we fetch the next verge pixel mask INX \ Increment X so we store in the next byte of four CPX #4 \ Loop back until we have populated all four bytes BNE dver16 \ ****************************************************************************** \ \ Name: DrawSegmentEdge (Part 4 of 7) \ Type: Subroutine \ Category: Drawing the track \ Summary: Set variables to use when updating the background colour \ Deep dive: Drawing the track verges \ \ ------------------------------------------------------------------------------ \ \ This part sets variables for use when updating the background colour table, \ and clips the pitch angle in RR to fit into a track line. \ \ ****************************************************************************** LDA vergeType \ Set T = vergeType << 3 ASL A \ ASL A \ So T is as follows: ASL A \ STA T \ * %00000000 if we are drawing leftVergeStart (%00) \ \ * %00001000 if we are drawing leftTrackStart (%01) \ \ * %00010000 if we are drawing rightVergeStart (%10) \ \ * %00011000 if we are drawing rightGrassStart (%11) LDA objectPalette \ Set A to the first verge pixel mask that we stored in \ part 3, which is always a four-pixel, single colour \ pixel byte of the form %aaaabbbb, where the pixel \ colour is %ab LSR A \ Set A = A >> 3 AND %11 LSR A \ = %aaaab AND %11 LSR A \ = %ab AND #%00000011 \ \ So A contains the colour of the pixels in the first \ verge pixel mask that we stored in part 3 ORA T \ Set A = A OR T \ = %ab OR %vv000 \ = %vv0ab \ \ where %vv is the verge we are currently drawing, from \ vergeType ORA #%01000000 \ Set backgroundRight = %010vv0ab STA backgroundRight \ \ So backgroundRight contains the following data: \ \ * %vv is the verge we are currently drawing, from \ vergeType \ \ * %ab is the colour in the first verge pixel mask \ for the verge \ \ * %010xx0xx denotes that this value (if used) gets \ stored in the backgroundColour table by the \ UpdateBackground routine \ \ We use backgroundRight in the UpdateBackground routine \ when updating the background colour for the track line LDA objectPalette \ Set A to the first verge pixel mask that we stored in \ part 3, which is always a four-pixel, single colour \ pixel byte BNE dver17 \ If A is non-zero, then the colour is not black, so \ jump to dver17 LDA #&55 \ A is zero, which is a four-pixel byte in black, so STA objectPalette \ store &55 in objectPalette, as &55 in the screen \ buffer represents black .dver17 STA JJ \ Store A in JJ, so it can be used as the right pixel \ byte when drawing the edge in the screen buffer, i.e. \ the fill byte to the right of the edge LDA objectPalette+3 \ Set A to the fourth verge pixel mask that we stored in \ part 3, which is in the form %aaaxbbbx, where %a is \ the first bit of the left colour in the mask, and %b \ is the second bit \ \ This works because the fourth entry in each block in \ vergePixelMask has pixels 0 to 2 set to the left \ colour within the four-pixel block, and only pixel 3 \ set to the other colour, and the following batch of \ bit-shuffling extracts the colour of pixel 2 LSR A \ Set bit 0 of A to bit 1 of the pixel mask AND #%00000001 BIT objectPalette+3 \ If bit 7 of the pixel mask is clear, jump to dver18 to BPL dver18 \ skip the following instruction ORA #%00000010 \ Set bit 1 of A, so bit 1 of A is the same as bit 7 of \ the fourth pixel mask \ \ In other words, A = %000000ab .dver18 ORA #%10000000 \ Set A = %100000ab ORA T \ Set A = A OR T \ = %100000ab OR %vv000 \ = %100vv0ab \ \ where %vv is the verge we are currently drawing, from \ vergeType STA backgroundLeft \ Set backgroundLeft = %100vv0ab \ \ So backgroundLeft contains the following data: \ \ * %vv is the verge we are currently drawing, from \ vergeType \ \ * %ab is the colour of the first three pixels in the \ verge pixel mask for the verge in objectPalette+3 \ \ * %010xx0xx denotes that this value (if used) gets \ stored in the backgroundColour table by the \ UpdateBackground routine \ \ We use backgroundLeft in the UpdateBackground routine \ when updating the background colour for the track \ line, and in part 6 where we extract the colour bits \ in %ab LDA thisPitchIndex \ If thisPitchIndex + 1 = vergeBufferEnd, then this is CLC \ the last entry in the verge buffer, so jump to dver19 ADC #1 \ to skip the following CMP vergeBufferEnd BEQ dver19 \ We now clip the pitch angle in RR so it fits into the \ track line range of 0 to 79 LDA RR \ If RR < 80, then this is a valid track line number, so CMP #80 \ jump to dver20 to leave it alone BCC dver20 .dver19 \ If we get here then RR is outside the correct range \ for track lines of 0 to 79, so we need to clip it to \ 0 or 79, depending on which way the line is heading LDA #0 \ Set A = 0 to use as the new value of R if WW is \ negative BIT WW \ If WW is negative, then the line is heading down the BMI dver20 \ screen, so jump to dver20 to set RR = 0, which clips \ RR to the bottom of the track view LDA #79 \ WW is positive, so the line is heading up the \ screen, so set A = 79 to use as the new value of R, \ which clips RR to the top of the track view .dver20 STA RR \ Update RR to be within the range 0 to 79 \ ****************************************************************************** \ \ Name: DrawSegmentEdge (Part 5 of 7) \ Type: Subroutine \ Category: Drawing the track \ Summary: Calculate the dash data block and screen addresses for the edge \ Deep dive: Drawing the track verges \ \ ------------------------------------------------------------------------------ \ \ This part calculates the dash data block for the edge, and the corresponding \ addresses within the screen buffer. \ \ ****************************************************************************** \ At this point, M is set to 128 + yaw angle * 4 for \ the previous edge LDA M \ Set U = M - 48 SEC \ SBC #48 \ So U and A now contain the pixel x-coordinate of the STA U \ previous edge, i.e. of the pixel at yaw angle M, \ because: \ \ M - 48 \ = 128 + (yaw angle * 4) - 48 \ = 80 + (yaw angle * 4) \ \ 80 is the x-coordinate of the middle of the screen, so \ this calculates the pixel x-coordinate for this yaw \ angle, in the range 0 to 159 LSR A \ Set UU = A / 4 LSR A \ STA UU \ So UU and A now contain the dash data block number for \ the pixel at yaw angle M, as each dash data block is \ four pixels wide CMP #40 \ If A >= 40, then this is not a valid dash data block BCS dver24 \ number, as they are in the range 0 to 39, so jump to \ dver28 via dver24 to clean up and return from the \ subroutine LSR A \ Halve A, as there are two dash data blocks in every \ page of memory, so A is now the page number of the \ relevant dash data block CLC \ Set Q = dashData + A ADC #HI(dashData) \ STA Q \ So Q is the high byte of the address of the dash data \ block STA S \ Set S = Q CLC \ Set NN = S + 1 ADC #1 STA NN LDA U \ Set X = U mod 8 AND #7 \ TAX \ So X contains the pixel number within two pixel bytes, \ i.e. in the range 0 to 7, as each pixel byte contains \ four pixels \ \ We pass this to the drawing routines so they start \ drawing from the correct pixel LDY N \ Set Y = N, which is the pitch angle of the previous \ edge \ By this point we have the following addresses set up: \ \ * (S R) = address of the first dash data block in \ the memory page containing the pixels at \ yaw angle M (as R = 0), i.e. of the \ previous edge \ \ * (Q P) = address of the second dash data block in \ the memory page containing the pixels at \ yaw angle M (as P = &80), i.e. of the \ previous edge \ \ * (NN MM) = address of the third dash data block in \ this sequence, i.e. the first memory \ page of the next dash data block (as \ NN = SS + 1) \ \ and the following variables: \ \ * X = pixel number (0 to 7) \ \ * Y = pitch angle of previous edge \ \ * UU = the number of the dash data block containing \ the previous edge, i.e. the dash data block \ where we start drawing the new edge \ \ * JJ = The right pixel byte when drawing the edge in \ the screen buffer, i.e. the fill byte to the \ right of the edge \ \ * RR = the pitch angle of the edge to draw, clipped \ to the range 0 to 79 (to map onto a track \ line) \ ****************************************************************************** \ \ Name: DrawSegmentEdge (Part 6 of 7) \ Type: Subroutine \ Category: Drawing the track \ Summary: Calculate the dash data block for the edge \ Deep dive: Drawing the track verges \ \ ------------------------------------------------------------------------------ \ \ This part draws the edge for this segment by jumping to the correct drawing \ routine, depending on the gradient and direction of the edge. \ \ ****************************************************************************** LDA SS \ If SS < TT, then the edge has a steep gradient, so CMP TT \ jump to dver26 to jump to the correct drawing routine BCC dver26 LDA objectPalette \ Set A to the first verge pixel mask that we stored in \ part 3, which is always a four-pixel, single colour \ pixel byte CMP #%11111111 \ If the background colour of the verge pixel mask is BEQ dver21 \ green, jump to dver21 to skip the following LDA backgroundLeft \ Extract bits 0-1 from backgroundLeft, which contain AND #%00000011 \ the colour from the left side of the verge pixel mask, \ i.e. the pixels on the left side of this verge edge CMP #3 \ If %ab = 3, so the colour to the left of the verge BEQ dver21 \ edge is green, jump to dver21 to skip the following \ Otherwise, disable the DrawGrass routines LDA #&60 \ Set A to the opcode for the RTS instruction STA DrawGrassLeft \ Modify the DrawGrassLeft routine to start with an RTS, \ so it returns without doing anything (so it's \ effectively disabled) STA DrawGrassRight \ Modify the DrawGrassRight routine to start with an \ RTS, so it returns without doing anything (so it's \ effectively disabled) BNE dver23 \ Jump to dver23 (this BNE is effectively a JMP as A is \ never zero) .dver21 LDA #&E0 \ Set A to the opcode for the CPX #128 instruction STA DrawGrassLeft \ Revert the DrawGrassLeft routine to start with a CPX \ instruction, so it runs as normal STA DrawGrassRight \ Revert the DrawGrassRight routine start with a CPX \ instruction, so it runs as normal LDA vergeType \ Set A to the type of verge we are drawing CMP #2 \ Set bit 7 of A if vergeType >= 2, so we get: ROR A \ \ * Bit 7 clear when we are drawing leftVergeStart or \ leftTrackStart \ \ * Bit 7 set when we are drawing rightVergeStart or \ rightGrassStart EOR VV \ If bit 7 of VV is the same as bit 7 of A, then: BPL dver23 \ \ * We are drawing the left track verge and drawing \ the edge from left to right (both bits are clear) \ \ * We are drawing the right track verge and drawing \ the edge from right to left (both bits are set) \ \ In either case, jump to dver23 to skip modifying the \ DrawVergeByteLeft and DrawVergeByteRight routines \ If we get here, then: \ \ * We are drawing the left track verge and drawing \ the edge from right to left \ \ * We are drawing the right track verge and drawing \ the edge from left to right \ \ We now modify both the DrawVergeByteLeft and \ DrawVergeByteRight routines to increment or decrement \ Y on entry, and to do nothing on exit (which flips the \ modifications we did in part 3, which made them do \ nothing on entry and the update of Y on exit) LDA verl5 \ Fetch the instruction that the DrawVergeByteLeft and \ DrawVergeByteRight routines currently execute on exit \ (which will be INY or DEY) STA verl1 \ Modify the instruction at verl1, so the \ DrawVergeByteLeft routine now does this action on \ entry instead STA verb1 \ Modify the instruction at verb1, so the \ DrawVergeByteRight routine now does this action on \ entry instead LDA #&EA \ Set A to the opcode for the NOP instruction STA verl5 \ Modify the instruction at verl5, so the \ DrawVergeByteLeft routine now does nothing on exit STA verb5 \ Modify the instruction at verb5, so the \ DrawVergeByteRight routine now does nothing on exit \ We also do the first increment or decrement, to get \ things set up correctly, depending on the sign in WW, \ which determines whether the edge is going up or down \ the screen LDA WW \ If WW is positive, then the line is heading up the BPL dver22 \ screen, to jump to dver22 to decrement Y before \ drawing the edge INY \ WW is negative, so the line is heading down the \ screen, so increment Y JMP dver23 \ Jump to dver23 to draw the edge .dver22 DEY \ WW is positive, so decrement Y and fall through into \ dver23 to draw the edge .dver23 \ If we get here then SS >= TT, so the edge has a \ gradient shallow LDA VV \ If VV is positive, jump to dver25 to draw the edge BPL dver25 \ from left to right JSR DrawShallowToLeft \ VV is negative, so draw the edge from right to left .dver24 JMP dver28 \ Jump to dver28 to clean up and return from the \ subroutine .dver25 JSR DrawShallowToRight \ VV is positive, so draw the edge from left to right JMP dver28 \ Jump to dver28 to clean up and return from the \ subroutine .dver26 \ If we get here then SS < TT, so the edge has a steep \ gradient LDA VV \ If VV is positive, jump to dver27 to draw the edge BPL dver27 \ from left to right JSR DrawSteepToLeft \ VV is negative, so draw the edge from right to left JMP dver28 \ Jump to dver28 to clean up and return from the \ subroutine .dver27 JSR DrawSteepToRight \ VV is positive, so draw the edge from left to right \ ****************************************************************************** \ \ Name: DrawSegmentEdge (Part 7 of 7) \ Type: Subroutine \ Category: Drawing the track \ Summary: Save the angles for the next call to DrawSegmentEdge and return \ from the subroutine \ Deep dive: Drawing the track verges \ \ ------------------------------------------------------------------------------ \ \ This part gets things ready for the next call to DrawSegmentEdge, when we draw \ the next segment's verge edge, and returns from the subroutine. \ \ ****************************************************************************** .dver28 LDA vergeOnScreenEdge \ If bit 7 of vergeOnScreenEdge is set then the edge we BMI dver29 \ are drawing is partially off-screen, so jump to dver29 \ to skip the following \ If we get here then the edge we are drawing is wholly \ on-screen, so we can store the edge's angles in M and \ N to be picked up in the next call to DrawSegmentEdge LDA W \ Set M = W, so we pass on 128 + yaw angle * 4 STA M LDA RR \ Set N = RR, so we pass on the pitch angle STA N .dver29 LDX thisYawIndex \ Set X to the original yaw angle index that we stored \ above, so X is preserved through calls to the routine LDY thisPitchIndex \ Set Y to the original pitch angle index that we stored \ above, so Y is preserved through calls to the routine RTS \ Return from the subroutine EQUB &A5, &53 \ These bytes appear to be unused EQUB &F0, &EB EQUB &20, &12 EQUB &2F, &4C EQUB &FC, &2C \ ****************************************************************************** \ \ Name: DrawShallowToRight \ Type: Subroutine \ Category: Drawing the track \ Summary: Draw a verge edge with a shallow gradient from left to right \ Deep dive: Drawing the track verges \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The pixel number to start drawing from, in the range \ 0 to 7, as it is measured across the two edge bytes that \ we draw in this routine \ \ Y The track line to draw on (0 to 79) \ \ SS The scaled yaw delta along the edge to draw \ \ TT The scaled pitch delta along the edge to draw \ \ RR The pitch angle of the current segment (we stop drawing \ the edge when Y reaches this value) \ \ UU The number of the dash data block where we start drawing \ \ JJ The right pixel byte when drawing the edge in the screen \ buffer, i.e. the fill byte to the right of the edge \ \ (S R) Address of the first dash data block in the memory page \ containing the pixels at the start of the previous edge \ \ (Q P) Address of the second dash data block in the memory page \ containing the pixels at the start of the previous edge \ \ (NN MM) Address of the third dash data block in this sequence, \ i.e. the first memory page of the next dash data block \ \ ****************************************************************************** .DrawShallowToRight LDA jumpShallowRight,X \ Modify the BCC instruction at shlr1 below so that STA shlr1+1 \ it jumps to the destination given in the X-th entry in \ the jumpShallowRight lookup table LDX #128 \ Set X = 128, so we can detect at the end of the \ routine whether X has been altered (which only happens \ if we draw something) LDA SS \ Set A = -SS EOR #&FF \ CLC \ This is the starting value for the cumulative slope ADC #1 \ error, which we tally up in A CLC \ Clear the C flag so the next instruction effectively \ becomes a JMP .shlr1 BCC shlr2 \ This instruction was modified above, so it jumps to \ the address specified in the jumpShallowRight table, \ as follows: \ \ * shlr4 when X = 0 \ * shlr6 when X = 1 \ * shlr8 when X = 2 \ * shlr10 when X = 3 \ * shlr13 when X = 4 \ * shlr15 when X = 5 \ * shlr17 when X = 6 \ * shlr19 when X = 7 \ \ These jump-points start by drawing a pixel byte before \ moving on to the slope error calculations \ \ The following entry points are also supported, though \ these don't appear to be used, as the routine is only \ called with X in the range 0 to 7: \ \ * shlr3 when X = 8 \ * shlr5 when X = 9 \ * shlr7 when X = 10 \ * shlr9 when X = 11 \ * shlr12 when X = 12 \ * shlr14 when X = 13 \ * shlr16 when X = 14 \ * shlr18 when X = 15 \ \ These jump-points start with the slope error \ calculations rather than a call to the draw routine, \ they would presumably omit the first pixel byte of the \ line, were they to be used .shlr2 LDX #128 \ Set X = 128, so we can detect at the end of the \ routine whether X has been altered (which only happens \ if we draw something) .shlr3 \ We start with the slope error calculation for pixel 0 \ of the first dash data block ADC TT \ Set A = A + TT, to add the pitch delta to the slope \ error BCC shlr5 \ If the addition didn't overflow, skip to the next \ pixel to step along the x-axis, as the cumulative \ pitch deltas haven't yet added up to a multiple of SS \ If we get here then the pitch deltas have added up to \ a whole line and the addition has overflowed, so we \ need to draw a pixel SBC SS \ If we get here then the cumulative pitch deltas in TT \ have added up to a multiple of the yaw delta in SS, so \ we subtract SS from the slope error (which we can do \ as we know the C flag is set) .shlr4 LDX #0 \ Draw the edge in pixel 0 of the first dash data block JSR DrawVergeByteLeft \ (the leftmost pixel) and draw a fill byte in the \ second dash data block \ \ If we have reached the pitch angle of the current \ segment, then we have reached the end of the edge to \ draw, in which case DrawVergeByteLeft will jump to \ StopDrawingEdge to abort this edge and return us two \ routines up the call stack, to DrawSegmentEdge .shlr5 ADC TT \ Repeat the slope error calculation for pixel 1 BCC shlr7 \ of the first dash data block SBC SS .shlr6 LDX #1 \ Draw the edge in pixel 1 of the first dash data block JSR DrawVergeByteLeft \ (the second pixel) and draw a fill byte in the second \ dash data block, or abort the drawing and return to \ DrawSegmentEdge if we've finished drawing the edge .shlr7 ADC TT \ Repeat the slope error calculation for pixel 2 BCC shlr9 \ of the first dash data block SBC SS .shlr8 LDX #2 \ Draw the edge in pixel 2 of the first dash data block JSR DrawVergeByteLeft \ (the third pixel) and draw a fill byte in the second \ dash data block, or abort the drawing and return to \ DrawSegmentEdge if we've finished drawing the edge .shlr9 ADC TT \ Repeat the slope error calculation for pixel 3 BCC shlr11 \ of the first dash data block SBC SS .shlr10 LDX #3 \ Draw the edge in pixel 3 of the first dash data block JSR DrawVergeByteLeft \ (the rightmost pixel) and draw a fill byte in the \ second dash data block, or abort the drawing and \ return to DrawSegmentEdge if we've finished drawing \ the edge .shlr11 JSR DrawGrassLeft \ If nothing has been drawn in the first dash data \ block (in which case X will still be 128), then fill \ the byte with green grass INC UU \ Increment UU so we now draw in the right pair of dash \ data blocks (i.e. the second and third dash data \ blocks of the three that we set up) .shlr12 ADC TT \ Repeat the slope error calculation for pixel 0 BCC shlr14 \ of the second dash data block SBC SS .shlr13 LDX #0 \ Draw the edge in pixel 0 of the second dash data block JSR DrawVergeByteRight \ (the leftmost pixel) and draw a fill byte in the third \ dash data block, or abort the drawing and return to \ DrawSegmentEdge if we've finished drawing the edge .shlr14 ADC TT \ Repeat the slope error calculation for pixel 1 BCC shlr16 \ of the second dash data block SBC SS .shlr15 LDX #1 \ Draw the edge in pixel 1 of the second dash data block JSR DrawVergeByteRight \ (the second pixel) and draw a fill byte in the third \ dash data block, or abort the drawing and return to \ DrawSegmentEdge if we've finished drawing the edge .shlr16 ADC TT \ Repeat the slope error calculation for pixel 2 BCC shlr18 \ of the second dash data block SBC SS .shlr17 LDX #2 \ Draw the edge in pixel 2 of the second dash data block JSR DrawVergeByteRight \ (the third pixel) and draw a fill byte in the third \ dash data block, or abort the drawing and return to \ DrawSegmentEdge if we've finished drawing the edge .shlr18 ADC TT \ Repeat the slope error calculation for pixel 3 BCC shlr20 \ of the second dash data block SBC SS .shlr19 LDX #3 \ Draw the edge in pixel 3 of the second dash data block JSR DrawVergeByteRight \ (the rightmost pixel) and draw a fill byte in the \ third dash data block, or abort the drawing and return \ to DrawSegmentEdge if we've finished drawing the edge .shlr20 JSR DrawGrassRight \ If nothing has been drawn in the second dash data \ block (in which case X will still be 128), then fill \ the byte with green grass INC S \ Increment S to move (S R) on to the next page, to move \ it right by two dash data blocks INC Q \ Increment Q to move (Q P) on to the next page, to move \ it right by two dash data blocks INC NN \ Increment NN to move (NN MM) on to the next page, to \ move it right by two dash data blocks INC UU \ Increment UU to the number of the next dash data block \ to the right LDX S \ If (S R) hasn't yet reached the rightmost dash data CPX #HI(dashData39)+1 \ block (i.e. it hasn't gone past block 39, which is at BNE shlr2 \ the right edge of the screen), then jump back to shlr2 \ to keep drawing RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: DrawShallowToLeft \ Type: Subroutine \ Category: Drawing the track \ Summary: Draw a verge edge with a shallow gradient from right to left \ Deep dive: Drawing the track verges \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The pixel number to start drawing from, in the range \ 0 to 7, as it is measured across the two edge bytes that \ we draw in this routine \ \ Y The track line to draw on (0 to 79) \ \ SS The scaled yaw delta along the edge to draw \ \ TT The scaled pitch delta along the edge to draw \ \ RR The pitch angle of the current segment (we stop drawing \ the edge when Y reaches this value) \ \ UU The number of the dash data block where we start drawing \ \ JJ The right pixel byte when drawing the edge in the screen \ buffer, i.e. the fill byte to the right of the edge \ \ (S R) Address of the first dash data block in the memory page \ containing the pixels at the start of the previous edge \ \ (Q P) Address of the second dash data block in the memory page \ containing the pixels at the start of the previous edge \ \ (NN MM) Address of the third dash data block in this sequence, \ i.e. the first memory page of the next dash data block \ \ ****************************************************************************** .DrawShallowToLeft LDA jumpShallowLeft,X \ Modify the BCC instruction at shrl1 below so that STA shrl1+1 \ it jumps to the destination given in the X-th entry in \ the jumpShallowLeft lookup table LDX #128 \ Set X = 128, so we can detect at the end of the \ routine whether X has been altered (which only happens \ if we draw something) LDA SS \ Set A = -SS EOR #&FF \ CLC \ This is the starting value for the cumulative slope ADC #1 \ error, which we tally up in A CLC \ Clear the C flag so the next instruction effectively \ becomes a JMP .shrl1 BCC shrl2 \ This instruction was modified above, so it jumps to \ the address specified in the jumpShallowLeft table, as \ follows: \ \ * shrl19 when X = 0 \ * shrl17 when X = 1 \ * shrl15 when X = 2 \ * shrl13 when X = 3 \ * shrl10 when X = 4 \ * shrl8 when X = 5 \ * shrl6 when X = 6 \ * shrl4 when X = 7 \ \ These jump-points start by drawing a pixel byte before \ moving on to the slope error calculations \ \ The following entry points are also supported, though \ these don't appear to be used, as the routine is only \ called with X in the range 0 to 7: \ \ * shrl18 when X = 8 \ * shrl16 when X = 9 \ * shrl14 when X = 10 \ * shrl12 when X = 11 \ * shrl9 when X = 12 \ * shrl7 when X = 13 \ * shrl5 when X = 14 \ * shrl3 when X = 15 \ \ These jump-points start with the slope error \ calculations rather than a call to the draw routine, \ they would presumably omit the first pixel byte of the \ line, were they to be used .shrl2 LDX #128 \ Set X = 128, so we can detect at the end of the \ routine whether X has been altered (which only happens \ if we draw something) .shrl3 \ We start with the slope error calculation for pixel 3 \ of the second dash data block ADC TT \ Set A = A + TT, to add the pitch delta to the slope \ error BCC shrl5 \ If the addition didn't overflow, skip to the next \ pixel to step along the x-axis, as the cumulative \ pitch deltas haven't yet added up to a multiple of SS \ If we get here then the pitch deltas have added up to \ a whole line and the addition has overflowed, so we \ need to draw a pixel SBC SS \ If we get here then the cumulative pitch deltas in TT \ have added up to a multiple of the yaw delta in SS, so \ we subtract SS from the slope error (which we can do \ as we know the C flag is set) .shrl4 LDX #3 \ Draw the edge in pixel 3 of the second dash data block JSR DrawVergeByteRight \ (the rightmost pixel) and draw a fill byte in the \ third dash data block \ \ If we have reached the pitch angle of the current \ segment, then we have reached the end of the edge to \ draw, in which case DrawVergeByteRight will jump to \ StopDrawingEdge to abort this edge and return us two \ routines up the call stack, to DrawSegmentEdge .shrl5 ADC TT \ Repeat the slope error calculation for pixel 2 BCC shrl7 \ of the second dash data block SBC SS .shrl6 LDX #2 \ Draw the edge in pixel 2 of the second dash data block JSR DrawVergeByteRight \ (the third pixel) and draw a fill byte in the third \ dash data block, or abort the drawing and return to \ DrawSegmentEdge if we've finished drawing the edge .shrl7 ADC TT \ Repeat the slope error calculation for pixel 1 BCC shrl9 \ of the second dash data block SBC SS .shrl8 LDX #1 \ Draw the edge in pixel 1 of the second dash data block JSR DrawVergeByteRight \ (the second pixel) and draw a fill byte in the third \ dash data block, or abort the drawing and return to \ DrawSegmentEdge if we've finished drawing the edge .shrl9 ADC TT \ Repeat the slope error calculation for pixel 0 BCC shrl11 \ of the second dash data block SBC SS .shrl10 LDX #0 \ Draw the edge in pixel 0 of the second dash data block JSR DrawVergeByteRight \ (the leftmost pixel) and draw a fill byte in the third \ dash data block, or abort the drawing and return to \ DrawSegmentEdge if we've finished drawing the edge .shrl11 JSR DrawGrassRight \ If nothing has been drawn in the second dash data \ block (in which case X will still be 128), then fill \ the byte with green grass DEC UU \ Decrement UU so we now draw in the left pair of dash \ data blocks (i.e. the first and second dash data \ blocks of the three that we set up) .shrl12 ADC TT \ Repeat the slope error calculation for pixel 3 BCC shrl14 \ of the first dash data block SBC SS .shrl13 LDX #3 \ Draw the edge in pixel 3 of the first dash data block JSR DrawVergeByteLeft \ (the rightmost pixel) and draw a fill byte in the \ second dash data block, or abort the drawing and \ return to DrawSegmentEdge if we've finished drawing \ the edge .shrl14 ADC TT \ Repeat the slope error calculation for pixel 2 BCC shrl16 \ of the first dash data block SBC SS .shrl15 LDX #2 \ Draw the edge in pixel 2 of the first dash data block JSR DrawVergeByteLeft \ (the third pixel) and draw a fill byte in the second \ dash data block, or abort the drawing and return to \ DrawSegmentEdge if we've finished drawing the edge .shrl16 ADC TT \ Repeat the slope error calculation for pixel 1 BCC shrl18 \ of the first dash data block SBC SS .shrl17 LDX #1 \ Draw the edge in pixel 1 of the first dash data block JSR DrawVergeByteLeft \ (the second pixel) and draw a fill byte in the second \ dash data block, or abort the drawing and return to \ DrawSegmentEdge if we've finished drawing the edge .shrl18 ADC TT \ Repeat the slope error calculation for pixel 0 BCC shrl20 \ of the first dash data block SBC SS .shrl19 LDX #0 \ Draw the edge in pixel 0 of the first dash data block JSR DrawVergeByteLeft \ (the leftmost pixel) and draw a fill byte in the \ second dash data block, or abort the drawing and \ return to DrawSegmentEdge if we've finished drawing \ the edge .shrl20 JSR DrawGrassLeft \ If nothing has been drawn in the first dash data \ block (in which case X will still be 128), then fill \ the byte with green grass DEC S \ Decrement S to move (S R) on to the previous page, to \ move it left by two dash data blocks DEC Q \ Decrement Q to move (Q P) on to the previous page, to \ move it left by two dash data blocks DEC NN \ Decrement NN to move (NN MM) on to the previous page, \ to move it left by two dash data blocks DEC UU \ Decrement UU to the number of the previous dash data \ block to the left LDX S \ If (S R) hasn't yet reached the leftmost dash data CPX #HI(dashData0)-1 \ block (i.e. it hasn't gone past block 0, which is at CLC \ the left edge of the screen), then jump back to shrl2 BNE shrl2 \ to keep drawing (with the C flag cleared, as it gets \ set by the comparison) JMP strl10 \ Jump to strl10 to finish drawing the right-hand pixel \ byte of the pair, as the first dash data block in \ (S R) might be off-screen, but the second one in (Q P) \ isn't \ ****************************************************************************** \ \ Name: DrawSteepToRight \ Type: Subroutine \ Category: Drawing the track \ Summary: Draw a verge edge with a steep gradient from left to right \ Deep dive: Drawing the track verges \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The pixel number to start drawing from, in the range \ 0 to 7, as it is measured across the two edge bytes that \ we draw in this routine \ \ Y The track line to draw on (0 to 79) \ \ SS The scaled yaw delta along the edge to draw \ \ TT The scaled pitch delta along the edge to draw \ \ RR The pitch angle of the current segment (we stop drawing \ the edge when Y reaches this value) \ \ UU The number of the dash data block where we start drawing \ \ JJ The right pixel byte when drawing the edge in the screen \ buffer, i.e. the fill byte to the right of the edge \ \ (S R) Address of the first dash data block in the memory page \ containing the pixels at the start of the previous edge \ \ (Q P) Address of the second dash data block in the memory page \ containing the pixels at the start of the previous edge \ \ (NN MM) Address of the third dash data block in this sequence, \ i.e. the first memory page of the next dash data block \ \ ****************************************************************************** .DrawSteepToRight LDA jumpSteepRight,X \ Modify the BCC instruction at stlr1 below so that STA stlr1+1 \ it jumps to the destination given in the X-th entry in \ the jumpSteepRight lookup table LDA TT \ Set A = -TT EOR #&FF \ CLC \ This is the starting value for the cumulative slope ADC #1 \ error, which we tally up in A CLC \ Clear the C flag so the next instruction effectively \ becomes a JMP .stlr1 BCC stlr2 \ This instruction was modified above, so it jumps to \ the address specified in the jumpSteepRight table, as \ follows: \ \ * stlr2 when X = 0 \ * stlr3 when X = 1 \ * stlr4 when X = 2 \ * stlr5 when X = 3 \ * stlr6 when X = 4 \ * stlr7 when X = 5 \ * stlr8 when X = 6 \ * stlr9 when X = 7 .stlr2 LDX #0 \ Draw the edge in pixel 0 of the first dash data block JSR DrawVergeByteLeft \ (the leftmost pixel) and draw a fill byte in the \ second dash data block \ \ If we have reached the pitch angle of the current \ segment, then we have reached the end of the edge to \ draw, in which case DrawVergeByteLeft will jump to \ StopDrawingEdge to abort this edge and return us two \ routines up the call stack, to DrawSegmentEdge \ We now do the slope error calculation for pixel 0 of \ the first dash data block ADC SS \ Set A = A + SS, to add the yaw delta to the slope \ error BCC stlr2 \ If the addition didn't overflow, skip to the next \ pixel to step along the y-axis, as the cumulative \ pitch deltas haven't yet added up to a multiple of TT \ If we get here then the pitch deltas have added up to \ a whole line and the addition has overflowed, so we \ need to draw a pixel SBC TT \ If we get here then the cumulative yaw deltas in SS \ have added up to a multiple of the pitch delta in TT, \ so we subtract TT from the slope error (which we can \ do as we know the C flag is set) .stlr3 LDX #1 \ Draw the edge in pixel 1 of the first dash data block JSR DrawVergeByteLeft \ (the second pixel) and draw a fill byte in the second \ dash data block, or abort the drawing and return to \ DrawSegmentEdge if we've finished drawing the edge ADC SS \ Repeat the slope error calculation for pixel 1 BCC stlr3 \ of the first dash data block SBC TT .stlr4 LDX #2 \ Draw the edge in pixel 2 of the first dash data block JSR DrawVergeByteLeft \ (the third pixel) and draw a fill byte in the second \ dash data block, or abort the drawing and return to \ DrawSegmentEdge if we've finished drawing the edge ADC SS \ Repeat the slope error calculation for pixel 2 BCC stlr4 \ of the first dash data block SBC TT .stlr5 LDX #3 \ Draw the edge in pixel 3 of the first dash data block JSR DrawVergeByteLeft \ (the rightmost pixel) and draw a fill byte in the \ second dash data block, or abort the drawing and \ return to DrawSegmentEdge if we've finished drawing \ the edge ADC SS \ Repeat the slope error calculation for pixel 3 BCC stlr5 \ of the first dash data block SBC TT INC UU \ Increment UU so we now draw in the right pair of dash \ data blocks (i.e. the second and third dash data \ blocks of the three that we set up) .stlr6 LDX #0 \ Draw the edge in pixel 0 of the second dash data block JSR DrawVergeByteRight \ (the leftmost pixel) and draw a fill byte in the third \ dash data block, or abort the drawing and return to \ DrawSegmentEdge if we've finished drawing the edge ADC SS \ Repeat the slope error calculation for pixel 0 BCC stlr6 \ of the second dash data block SBC TT .stlr7 LDX #1 \ Draw the edge in pixel 1 of the second dash data block JSR DrawVergeByteRight \ (the second pixel) and draw a fill byte in the third \ dash data block, or abort the drawing and return to \ DrawSegmentEdge if we've finished drawing the edge ADC SS \ Repeat the slope error calculation for pixel 1 BCC stlr7 \ of the second dash data block SBC TT .stlr8 LDX #2 \ Draw the edge in pixel 2 of the second dash data block JSR DrawVergeByteRight \ (the third pixel) and draw a fill byte in the third \ dash data block, or abort the drawing and return to \ DrawSegmentEdge if we've finished drawing the edge ADC SS \ Repeat the slope error calculation for pixel 2 BCC stlr8 \ of the second dash data block SBC TT .stlr9 LDX #3 \ Draw the edge in pixel 3 of the second dash data block JSR DrawVergeByteRight \ (the rightmost pixel) and draw a fill byte in the \ third dash data block, or abort the drawing and return \ to DrawSegmentEdge if we've finished drawing the edge ADC SS \ Repeat the slope error calculation for pixel 2 BCC stlr9 \ of the second dash data block SBC TT INC S \ Increment S to move (S R) on to the next page, to move \ it right by two dash data blocks INC Q \ Increment Q to move (Q P) on to the next page, to move \ it right by two dash data blocks INC NN \ Increment NN to move (NN MM) on to the next page, to \ move it right by two dash data blocks INC UU \ Increment UU to the number of the next dash data block \ to the right LDX S \ If (S R) hasn't yet reached the rightmost dash data CPX #HI(dashData39)+1 \ block (i.e. it hasn't gone past block 39, which is at BNE stlr2 \ the right edge of the screen), then jump back to shlr2 \ to keep drawing RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: DrawSteepToLeft \ Type: Subroutine \ Category: Drawing the track \ Summary: Draw a verge edge with a steep gradient from right to left \ Deep dive: Drawing the track verges \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The pixel number to start drawing from, in the range \ 0 to 7, as it is measured across the two edge bytes that \ we draw in this routine \ \ Y The track line to draw on (0 to 79) \ \ SS The scaled yaw delta along the edge to draw \ \ TT The scaled pitch delta along the edge to draw \ \ RR The pitch angle of the current segment (we stop drawing \ the edge when Y reaches this value) \ \ UU The number of the dash data block where we start drawing \ \ JJ The right pixel byte when drawing the edge in the screen \ buffer, i.e. the fill byte to the right of the edge \ \ (S R) Address of the first dash data block in the memory page \ containing the pixels at the start of the previous edge \ \ (Q P) Address of the second dash data block in the memory page \ containing the pixels at the start of the previous edge \ \ (NN MM) Address of the third dash data block in this sequence, \ i.e. the first memory page of the next dash data block \ \ ------------------------------------------------------------------------------ \ \ Other entry points: \ \ strl10 Finish drawing the verge edge by updating the background \ table for this track line \ \ ****************************************************************************** .DrawSteepToLeft LDA jumpSteepLeft,X \ Modify the BCC instruction at strl1 below so that STA strl1+1 \ it jumps to the destination given in the X-th entry in \ the jumpSteepLeft lookup table LDA TT \ Set A = -TT EOR #&FF \ CLC \ This is the starting value for the cumulative slope ADC #1 \ error, which we tally up in A CLC \ Clear the C flag so the next instruction effectively \ becomes a JMP .strl1 BCC strl2 \ This instruction was modified above, so it jumps to \ the address specified in the jumpSteepLeft table, as \ follows: \ \ * strl9 when X = 0 \ * strl8 when X = 1 \ * strl7 when X = 2 \ * strl6 when X = 3 \ * strl5 when X = 4 \ * strl4 when X = 5 \ * strl3 when X = 6 \ * strl2 when X = 7 .strl2 LDX #3 \ Draw the edge in pixel 3 of the second dash data block JSR DrawVergeByteRight \ (the rightmost pixel) and draw a fill byte in the \ third dash data block \ \ If we have reached the pitch angle of the current \ segment, then we have reached the end of the edge to \ draw, in which case DrawVergeByteRight will jump to \ StopDrawingEdge to abort this edge and return us two \ routines up the call stack, to DrawSegmentEdge \ We now do the slope error calculation for pixel 3 of \ the second dash data block ADC SS \ Set A = A + SS, to add the yaw delta to the slope \ error BCC strl2 \ If the addition didn't overflow, skip to the next \ pixel to step along the y-axis, as the cumulative \ pitch deltas haven't yet added up to a multiple of TT \ If we get here then the pitch deltas have added up to \ a whole line and the addition has overflowed, so we \ need to draw a pixel SBC TT \ If we get here then the cumulative yaw deltas in SS \ have added up to a multiple of the pitch delta in TT, \ so we subtract TT from the slope error (which we can \ do as we know the C flag is set) .strl3 LDX #2 \ Draw the edge in pixel 2 of the second dash data block JSR DrawVergeByteRight \ (the third pixel) and draw a fill byte in the third \ dash data block, or abort the drawing and return to \ DrawSegmentEdge if we've finished drawing the edge ADC SS \ Repeat the slope error calculation for pixel 2 BCC strl3 \ of the second dash data block SBC TT .strl4 LDX #1 \ Draw the edge in pixel 1 of the second dash data block JSR DrawVergeByteRight \ (the second pixel) and draw a fill byte in the third \ dash data block, or abort the drawing and return to \ DrawSegmentEdge if we've finished drawing the edge ADC SS \ Repeat the slope error calculation for pixel 1 BCC strl4 \ of the second dash data block SBC TT .strl5 LDX #0 \ Draw the edge in pixel 0 of the second dash data block JSR DrawVergeByteRight \ (the leftmost pixel) and draw a fill byte in the third \ dash data block, or abort the drawing and return to \ DrawSegmentEdge if we've finished drawing the edge ADC SS \ Repeat the slope error calculation for pixel 0 BCC strl5 \ of the second dash data block SBC TT DEC UU \ Decrement UU so we now draw in the left pair of dash \ data blocks (i.e. the first and second dash data \ blocks of the three that we set up) .strl6 LDX #3 \ Draw the edge in pixel 3 of the first dash data block JSR DrawVergeByteLeft \ (the rightmost pixel) and draw a fill byte in the \ second dash data block, or abort the drawing and \ return to DrawSegmentEdge if we've finished drawing \ the edge ADC SS \ Repeat the slope error calculation for pixel 3 BCC strl6 \ of the first dash data block SBC TT .strl7 LDX #2 \ Draw the edge in pixel 2 of the first dash data block JSR DrawVergeByteLeft \ (the third pixel) and draw a fill byte in the second \ dash data block, or abort the drawing and return to \ DrawSegmentEdge if we've finished drawing the edge ADC SS \ Repeat the slope error calculation for pixel 2 BCC strl7 \ of the first dash data block SBC TT .strl8 LDX #1 \ Draw the edge in pixel 1 of the first dash data block JSR DrawVergeByteLeft \ (the second pixel) and draw a fill byte in the second \ dash data block, or abort the drawing and return to \ DrawSegmentEdge if we've finished drawing the edge ADC SS \ Repeat the slope error calculation for pixel 1 BCC strl8 \ of the first dash data block SBC TT .strl9 LDX #0 \ Draw the edge in pixel 0 of the first dash data block JSR DrawVergeByteLeft \ (the leftmost pixel) and draw a fill byte in the \ second dash data block, or abort the drawing and \ return to DrawSegmentEdge if we've finished drawing \ the edge ADC SS \ Repeat the slope error calculation for pixel 0 BCC strl9 \ of the first dash data block SBC TT DEC S \ Decrement S to move (S R) on to the previous page, to \ move it left by two dash data blocks DEC Q \ Decrement Q to move (Q P) on to the previous page, to \ move it left by two dash data blocks DEC NN \ Decrement NN to move (NN MM) on to the previous page, \ to move it left by two dash data blocks DEC UU \ Decrement UU to the number of the previous dash data \ block to the left LDX S \ If (S R) hasn't yet reached the leftmost dash data CPX #HI(dashData0)-1 \ block (i.e. it hasn't gone past block 0, which is at CLC \ the left edge of the screen), then jump back to strl2 BNE strl2 \ to keep drawing (with the C flag cleared, as it gets \ set by the comparison) .strl10 \ If we get here then we have drawn our steep edge to \ the left, all the way to the left edge of the screen, \ so we now need to update the background table for this \ track line \ \ First, though, we need to alter the track line in Y so \ it points to the correct line, which we do by moving \ the track line up or down by one step, in the same \ direction as the edge we are drawing LDA verl1 \ Fetch the instruction that is run at the start of the \ DrawVergeByteLeft routine, which will be an INY or DEY \ instruction (this is the instruction that is run on \ entry into the DrawVergeByteLeft routine, and which \ moves the track line on to the next line in the \ correct up/down direction for the edge we are drawing) STA strl11 \ Store the instruction we just fetched into the next \ address, so we execute the same instruction before \ falling into UpdateBackground .strl11 NOP \ This instruction is modified by the above code to be \ the same INY or DEY that is executed at the start of \ the DrawVergeByteLeft routine, so it moves the track \ line on by one in the correct direction for the edge \ we are drawing \ Fall through into UpdateBackground to update the \ background colour table \ ****************************************************************************** \ \ Name: UpdateBackground \ Type: Subroutine \ Category: Drawing the track \ Summary: Update the background colour table for when we draw a verge edge \ off the left edge of the screen \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ Y The track line we are drawing on (0 to 79) \ \ ****************************************************************************** .UpdateBackground LDA vergeOnScreenEdge \ If bit 7 of vergeOnScreenEdge is set then the edge we BMI upba1 \ are drawing is partially off-screen, so jump to upba1 \ to skip the following \ If we get here then the edge we are drawing is fully \ on-screen LDA backgroundRight \ Set A = backgroundRight, which is in the format \ %010vv0ab: \ \ * %vv is the verge we are currently drawing, from \ vergeType \ \ * %ab is the colour in the first verge pixel mask \ for the verge \ \ * %010xx0xx denotes that this value is being set to \ backgroundRight by the UpdateBackground routine JMP upba2 \ Jump to upba2 .upba1 \ If we get here then the edge we are drawing is \ partially off-screen DEY \ Decrement the track line in Y LDA backgroundColour,Y \ Set A to the contents of the background colour table \ for track line Y BNE upba4 \ If the background colour currently in the table is \ non-zero, jump to upba4 to return from the subroutine LDA backgroundLeft \ The background colour currently in the table is zero, \ so set A = backgroundLeft, which is in the format \ %100vv0ab: \ \ * %vv is the verge we are currently drawing, from \ vergeType \ \ * %ab is the colour of the first three pixels in the \ verge pixel mask for the verge in objectPalette+3 \ \ * %100xx0xx denotes that this value is being set to \ backgroundLeft by the UpdateBackground routine .upba2 CPY #80 \ If Y >= 80, then this is not a valid track line BCS upba4 \ number, so jump to upba4 to return from the subroutine LDX playerSideways \ If playerSideways < 40, then the player's car is CPX #40 \ facing along the track rather than sideways, so jump BCC upba3 \ to upba3 to store A in the background colour table \ If we get here then the player's car is facing \ sideways, relative to the track direction, so we set \ the background colour to either black or green, \ ignoring the colour of the verge marks STA T \ Store A in T so we can retrieve it below AND #%00000011 \ If bits 0-1 of A >= 3, i.e. bits 0-1 of A = 3, set the CMP #3 \ C flag LDA T \ Retrieve the value of A that we stored in T above BCS upba3 \ If bits 0-1 of A = 3, jump to upba3 to store green as \ the background colour AND #%11111100 \ Otherwise clear bits 0 and 1 of A, to store black as \ the background colour .upba3 STA backgroundColour,Y \ Store A as the background colour for track line Y .upba4 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: DrawVergeByteLeft \ Type: Subroutine \ Category: Drawing the track \ Summary: Draw two bytes into the screen buffer in the first and second dash \ data blocks for the edge \ Deep dive: Drawing the track verges \ \ ------------------------------------------------------------------------------ \ \ This routine draws a single byte of a verge edge in the first dash data block \ of the three that we set up in DrawSegmentEdge. It also draws a second byte to \ the right, in the second dash data block, using the fill colour so the screen \ gets filled to the right of the verge edge. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The pixel number of the edge within the pixel byte, with \ 0 being the first pixel (at the left end of the byte) to \ 3 as the last pixel (at the right end of the byte) \ \ Y The track line to draw on (0 to 79) \ \ RR The pitch angle of the current segment (we stop drawing \ the edge when Y reaches this value) \ \ UU The number of the dash data block where we start drawing \ \ JJ The right pixel byte when drawing the edge in the screen \ buffer, i.e. the fill byte to the right of the edge \ \ (S R) Address of the first dash data block in the memory page \ containing the pixels at the start of the previous edge \ \ (Q P) Address of the second dash data block in the memory page \ containing the pixels at the start of the previous edge \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ C flag The C flag is cleared \ \ Y Incremented or decremented, depending on which way the \ verge goes (decremented if the verge goes down, \ incremented is it goes up) \ \ A A is unchanged \ \ ****************************************************************************** .DrawVergeByteLeft STA II \ Store A in II so we can retrieve it later .verl1 NOP \ This instruction is modified by the DrawSegmentEdge \ routine, to be NOP, INY or DEY, to move on to the next \ track line as appropriate CPY RR \ If Y = RR, we have reached the pitch angle of the BEQ StopDrawingEdge \ current edge, so jump to StopDrawingEdge to stop \ drawing the edge LDA UU \ Set A = UU, the number of the dash data block where we \ want to draw .verl2 STA &7000,Y \ This instruction is modified by the DrawVergeEdge \ routine to point to the relevant table for the edge \ we are drawing: \ \ * leftVergeStart \ \ * leftTrackStart \ \ * rightVergeStart \ \ * rightGrassStart \ \ So this stores the dash data block number for the \ previous edge in the relevant verge table for track \ line Y LDA (R),Y \ Set A to the current contents of track line Y in \ (S R), which is the dash data block containing the \ previous edge BNE verl6 \ If the current contents of the screen buffer is \ non-zero, then it already contains something, so jump \ to verl6 to merge our edge with the existing pixels LDA objectPalette,X \ Set A to the X-th entry from the object palette, which \ contains a background colour byte with the first X \ pixels in the foreground colour .verl3 STA (R),Y \ Store the pixel byte in the dash data block LDA JJ \ Store the pixel byte in JJ in the next dash data block STA (P),Y \ to fill the pixel byte to the right of the edge byte \ with the background colour in JJ (so this fills to the \ right of the edge as we draw it) .verl4 LDA II \ Retrieve the value of A we stored above, so A is \ unchanged by the routine .verl5 INY \ This instruction is modified by the DrawSegmentEdge \ routine, to be NOP, INY or DEY, to move on to the next \ track line as appropriate CLC \ Clear the C flag RTS \ Return from the subroutine .verl6 \ If we get here then the pixel byte we are drawing into \ is non-empty CPY #44 \ If Y >= 44, jump to verl7 to skip the following check, BCS verl7 \ as offsets of 44 and above are always within a dash \ data block JSR CheckDashData \ Check whether offset Y points to dash data within \ block UU, clearing the C flag if it does BCC verl4 \ If offset Y does not point to dash data, jump to verl4 \ to return from the subroutine without drawing anything .verl7 \ If we get here then the pixel byte we are drawing into \ is non-empty and is a valid part of the screen buffer CMP #&55 \ If the current contents of the screen buffer is not BNE verl8 \ &55, then it does not denote black, so skip the \ following LDA #0 \ The current contents of the screen buffer is &55, \ which denotes black, so set A to 0 so we merge with a \ pixel byte of black .verl8 AND pixelsToLeft,X \ AND the current contents with the X-th pixel mask from \ pixelsToLeft, which is a pixel byte with all the \ pixels set to the left of the X-th pixel, so this \ clears all pixels in the existing screen buffer byte \ from the X-th pixel and to the right \ \ In other words, this clears the track edge and to the \ right, but keeps content to the left of the edge ORA vergeEdgeRight,X \ We set up vergeEdgeRight to contain the pixel bytes \ from objectPalette, but masked to only include the \ rightmost 4, 3, 2 and 1 pixels, so this inserts the \ edge and all the pixels to the right of the edge into \ the pixels that we just cleared, thus drawing the edge \ on top of what's already on-screen BNE verl3 \ If the resulting pixel byte in A is non-zero, then we \ are ready to poke it into the screen buffer, so jump \ to verl3 LDA #&55 \ The resulting pixel byte is zero, which is a black \ pixel byte, and we represent this in the screen buffer \ with &55, so set A to &55 so we poke this into the \ screen buffer instead of zero BNE verl3 \ Jump to verl3 (this BNE is effectively a JMP as A is \ never zero) \ ****************************************************************************** \ \ Name: StopDrawingEdge \ Type: Subroutine \ Category: Drawing the track \ Summary: Stop drawing the current segment's verge edge \ \ ------------------------------------------------------------------------------ \ \ This routine stops drawing the current segment's edge when called from \ DrawVergeByteLeft or DrawVergeByteRight. \ \ ****************************************************************************** .StopDrawingEdge TSX \ These instructions remove two bytes from the top of INX \ the stack so the RTS below returns an extra level up INX \ the call chain TXS \ \ We jump to this routine from the DrawVergeByteLeft and \ DrawVergeByteRight routines. These are only called \ from the DrawShallowToLeft, DrawShallowToRight, \ DrawSteepToLeft or DrawSteepToRight routines, which in \ turn are only called from DrawSegmentEdge, so this \ returns us to DrawSegmentEdge to stop drawing the \ current segment's verge edge and move on to the next \ segment LDA updateBackground \ If updateBackground is non-zero, then we just drew a BNE UpdateBackground \ verge on the start of the left edge of the screen, so \ call UpdateBackground to update the background colour \ for this track line, returning from the subroutine \ using a tail call RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: DrawVergeByteRight \ Type: Subroutine \ Category: Drawing the track \ Summary: Draw two bytes into the screen buffer in the second and third dash \ data blocks for the edge \ Deep dive: Drawing the track verges \ \ ------------------------------------------------------------------------------ \ \ This routine draws a single byte of a verge edge in the second dash data block \ of the three that we set up in DrawSegmentEdge. It also draws a second byte to \ the right, in the third dash data block, using the fill colour so the screen \ gets filled to the right of the verge edge. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The pixel number of the edge within the pixel byte, with \ 0 being the first pixel (at the left end of the byte) to \ 3 as the last pixel (at the right end of the byte) \ \ Y The track line to draw on (0 to 79) \ \ RR The pitch angle of the current segment (we stop drawing \ the edge when Y reaches this value) \ \ UU The number of the dash data block where we start drawing \ \ JJ The right pixel byte when drawing the edge in the screen \ buffer, i.e. the fill byte to the right of the edge \ \ (Q P) Address of the second dash data block in the memory page \ containing the pixels at the start of the previous edge \ \ (NN MM) Address of the third dash data block in this sequence, \ i.e. the first memory page of the next dash data block \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ C flag The C flag is cleared \ \ Y Incremented or decremented, depending on which way the \ verge goes (decremented if the verge goes down, \ incremented is it goes up) \ \ A A is unchanged \ \ ****************************************************************************** .DrawVergeByteRight STA II \ Store A in II so we can retrieve it later .verb1 NOP \ This instruction is modified by the DrawSegmentEdge \ routine, to be NOP, INY or DEY, to move on to the next \ track line as appropriate CPY RR \ If Y = RR, we have reached the pitch angle of the BEQ StopDrawingEdge \ current edge, so jump to StopDrawingEdge to stop \ drawing the edge LDA UU \ Set A = UU, the number of the dash data block where we \ want to draw .verb2 STA &7000,Y \ This instruction is modified by the DrawVergeEdge \ routine to point to the relevant table for the edge \ we are drawing: \ \ * leftVergeStart \ \ * leftTrackStart \ \ * rightVergeStart \ \ * rightGrassStart \ \ So this stores the dash data block number for the \ previous edge in the relevant verge table for track \ line Y LDA (P),Y \ Set A to the current contents of track line Y in \ (Q P), which is the dash data block containing the \ previous edge BNE verb6 \ If the current contents of the screen buffer is \ non-zero, then it already contains something, so jump \ to verb6 to merge our edge with the existing pixels LDA objectPalette,X \ Set A to the X-th entry from the object palette, which \ contains a background colour byte with the first X \ pixels in the foreground colour .verb3 STA (P),Y \ Store the pixel byte in the dash data block LDA JJ \ Store the pixel byte in JJ in the next dash data block STA (MM),Y \ to fill the pixel byte to the right of the edge byte \ with the background colour in JJ (so this fills to the \ right of the edge as we draw it) .verb4 LDA II \ Retrieve the value of A we stored above, so A is \ unchanged by the routine .verb5 INY \ This instruction is modified by the DrawSegmentEdge \ routine, to be NOP, INY or DEY, to move on to the next \ track line as appropriate CLC \ Clear the C flag RTS \ Return from the subroutine .verb6 \ If we get here then the pixel byte we are drawing into \ is non-empty CPY #44 \ If Y >= 44, jump to verb7 to skip the following check, BCS verb7 \ as offsets of 44 and above are always within a dash \ data block JSR CheckDashData \ Check whether offset Y points to dash data within \ block UU, clearing the C flag if it does BCC verb4 \ If offset Y does not point to dash data, jump to verb4 \ to return from the subroutine without drawing anything .verb7 \ If we get here then the pixel byte we are drawing into \ is non-empty and is a valid part of the screen buffer CMP #&55 \ If the current contents of the screen buffer is not BNE verb8 \ &55, then it does not denote black, so skip the \ following LDA #0 \ The current contents of the screen buffer is &55, \ which denotes black, so set A to 0 so we merge with a \ pixel byte of black .verb8 AND pixelsToLeft,X \ AND the current contents with the X-th pixel mask from \ pixelsToLeft, which is a pixel byte with all the \ pixels set to the left of the X-th pixel, so this \ clears all pixels in the existing screen buffer byte \ from the X-th pixel and to the right \ \ In other words, this clears the track edge and to the \ right, but keeps content to the left of the edge ORA vergeEdgeRight,X \ We set up vergeEdgeRight to contain the pixel bytes \ from objectPalette, but masked to only include the \ rightmost 4, 3, 2 and 1 pixels, so this inserts the \ edge and all the pixels to the right of the edge into \ the pixels that we just cleared, thus drawing the edge \ on top of what's already on-screen BNE verb3 \ If the resulting pixel byte in A is non-zero, then we \ are ready to poke it into the screen buffer, so jump \ to verb3 LDA #&55 \ The resulting pixel byte is zero, which is a black \ pixel byte, and we represent this in the screen buffer \ with &55, so set A to &55 so we poke this into the \ screen buffer instead of zero BNE verb3 \ Jump to verb3 (this BNE is effectively a JMP as A is \ never zero) \ ****************************************************************************** \ \ Name: DrawGrassRight \ Type: Subroutine \ Category: Drawing the track \ Summary: Draw a green byte into the screen buffer in the second dash data \ block for the edge \ Deep dive: Drawing the track verges \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The value of X from the current drawing routine: \ \ * 0 to 3 if a pixel byte has already been drawn \ \ * 128 is no pixel byte has been drawn so far \ \ Y The track line to draw on (0 to 79) \ \ (Q P) Address of the second dash data block in the memory page \ containing the pixels at the start of the previous edge \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ X 128 \ \ A A is unchanged \ \ C flag Clear \ \ ****************************************************************************** .DrawGrassRight CPX #128 \ If X <> 128 then the pixel byte has already been drawn BNE grar2 \ in the calling routine (i.e. DrawShallowToLeft or \ DrawShallowToRight), so jump to grar2 to return from \ the subroutine CPY #44 \ If Y >= 44, jump to grar1 to skip the following check, BCS grar1 \ as offsets of 44 and above are always within a dash \ data block JSR CheckDashData \ Check whether offset Y points to dash data within \ block UU, clearing the C flag if it does BCC grar2 \ If offset Y does not point to dash data, jump to grar2 \ to return from the subroutine .grar1 TAX \ Y points into dash data, so set the Y-th byte of (Q P) LDA #%11111111 \ to four pixels of colour 3 (green) STA (P),Y TXA .grar2 LDX #128 \ Set X = 128 to reset the draw detection logic for the \ next byte to draw CLC \ Clear the C flag RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: DrawGrassLeft \ Type: Subroutine \ Category: Drawing the track \ Summary: Draw a green byte into the screen buffer in the first dash data \ block for the edge \ Deep dive: Drawing the track verges \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The value of X from the current drawing routine: \ \ * 0 to 3 if a pixel byte has already been drawn \ \ * 128 is no pixel byte has been drawn so far \ \ Y The track line to draw on (0 to 79) \ \ (S R) Address of the first dash data block in the memory page \ containing the pixels at the start of the previous edge \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ X 128 \ \ A A is unchanged \ \ C flag Clear \ \ ****************************************************************************** .DrawGrassLeft CPX #128 \ If X <> 128 then the pixel byte has already been drawn BNE gral2 \ in the calling routine (i.e. DrawShallowToLeft or \ DrawShallowToRight), so jump to gral2 to return from \ the subroutine CPY #44 \ If Y >= 44, jump to gral1 to skip the following check, BCS gral1 \ as offsets of 44 and above are always within a dash \ data block JSR CheckDashData \ Check whether offset Y points to dash data within \ block UU, clearing the C flag if it does BCC gral2 \ If offset Y does not point to dash data, jump to gral2 \ to return from the subroutine .gral1 TAX \ Y points into dash data, so set the Y-th byte of (S R) LDA #%11111111 \ to four pixels of colour 3 (green) STA (R),Y TXA .gral2 LDX #128 \ Set X = 128 to reset the draw detection logic for the \ next byte to draw CLC \ Clear the C flag RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: CheckDashData \ Type: Subroutine \ Category: Screen buffer \ Summary: Check whether a dash data block index is pointing to dash data \ \ ------------------------------------------------------------------------------ \ \ This routine checks whether an index in Y, which is relative to the start of a \ dash data block, is pointing to dash data within the block. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ UU Dash data block number (0 to 39) \ \ Y The index from the start of the dash data block \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ C flag The result, as follows: \ \ * Clear if offset Y does not point to dash data \ \ * Set if offset Y does point to dash data \ \ A A is unchanged \ \ X X is unchanged \ \ ****************************************************************************** .CheckDashData STA T \ Store A and X in T and U so we can retrieve them below STX U LDX UU \ Set X to the dash data block number TYA \ Set the C flag as follows: CMP dashDataOffset,X \ \ * C flag clear if Y < dashDataOffset,X \ \ * C flag set if Y >= dashDataOffset,X BNE cdas1 \ If Y <> the dashDataOffset of block X, skip the \ following instruction CLC \ If we get here then Y = the dashDataOffset of block X, \ so clear the C flag \ So by this point we have: \ \ * C flag clear if Y <= dashDataOffset,X \ \ * C flag set if Y > dashDataOffset,X \ \ As the dash data lives at the top of each dash data \ block, this gives us the result we want (as Y is \ pointing to data when Y > dashDataOffset,X) .cdas1 LDA T \ Restore the values of A and X that we stored above LDX U RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: token26 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 26 \ Deep dive: Text tokens \ \ ****************************************************************************** .token26 EQUB 200 + 54 \ Print token 54 ("FORMULA 3 CHAMPIONSHIP" header) EQUB 31, 10, 12 \ Move text cursor to column 10, row 12 EQUB 131 \ Set foreground colour to yellow alphanumeric EQUS "STANDARD OF" \ Print "STANDARD OF" EQUB 200 + 15 \ Print token 15 (" RACE") EQUB 31, 14, 14 \ Move text cursor to column 14, row 14 EQUB 255 \ End token \ ****************************************************************************** \ \ Name: token4 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 4 \ Deep dive: Text tokens \ \ ****************************************************************************** .token4 EQUB 200 + 52 \ Print token 52 (multicoloured "REVS") EQUB 160 + 3 \ Print 3 spaces EQUB 200 + 52 \ Print token 52 (multicoloured "REVS") EQUB 160 + 3 \ Print 3 spaces EQUB 200 + 52 \ Print token 52 (multicoloured "REVS") EQUB 160 + 1 \ Print 1 space EQUB 255 \ End token \ ****************************************************************************** \ \ Name: dashData0 \ Type: Variable \ Category: Screen buffer \ Summary: Contains code that gets moved into screen memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData0 SKIP 52 \ Populated with code from &7FCC to &7FFF \ ****************************************************************************** \ \ Name: tyreEdgeIndex \ Type: Variable \ Category: Dashboard \ Summary: Index of the mask and pixel bytes for the tyre edges on a specific \ track line \ Deep dive: Drawing around the dashboard \ \ ------------------------------------------------------------------------------ \ \ This table points to the index of the mask or pixel byte to use from the \ leftTyreMask/rightTyreMask and leftTyrePixels/rightTyrePixels tables for a \ specific track line. This is used when clipping track lines to the tyre edges. \ \ There is a byte for each track line from 27 (the track line at the top of the \ tyres) down to 3 (the lowest track line, just above where the wing mirror \ joins the car body). Lines 0 to 2 are not used. \ \ ****************************************************************************** .tyreEdgeIndex EQUB 0 \ Line 0 EQUB 0 \ Line 1 EQUB 0 \ Line 2 EQUB 0 \ Line 3 EQUB 6 \ Line 4 EQUB 6 \ Line 5 EQUB 6 \ Line 6 EQUB 6 \ Line 7 EQUB 6 \ Line 8 EQUB 6 \ Line 9 EQUB 6 \ Line 10 EQUB 3 \ Line 11 EQUB 3 \ Line 12 EQUB 3 \ Line 13 EQUB 3 \ Line 14 EQUB 3 \ Line 15 EQUB 1 \ Line 16 EQUB 1 \ Line 17 EQUB 1 \ Line 18 EQUB 0 \ Line 19 EQUB 0 \ Line 20 EQUB 0 \ Line 21 EQUB 5 \ Line 22 EQUB 4 \ Line 23 EQUB 3 \ Line 24 EQUB 2 \ Line 25 EQUB 1 \ Line 26 EQUB 0 \ Line 27 \ ****************************************************************************** \ \ Name: segmentFlagMask \ Type: Variable \ Category: Track geometry \ Summary: A mask for extracting bits from the segment flag byte when \ processing the track verge and corner markers \ \ ****************************************************************************** .segmentFlagMask EQUB %00101101 \ Mask for the right verge, to keep the following from \ the segment flags: \ \ * Bit 0 (section shape) \ * Bit 2 (colour of right verge marks) \ * Bit 3 (show right corner markers) \ * Bit 5 (corner marker colours) EQUB %00110011 \ Mask for the left verge, to keep the following from \ the segment flags: \ \ * Bit 0 (section shape) \ * Bit 1 (colour of left verge marks) \ * Bit 4 (show left corner markers) \ * Bit 5 (corner marker colours) \ ****************************************************************************** \ \ Name: vergeColour \ Type: Variable \ Category: Track geometry \ Summary: Lookup table for converting bits 0-2 of the segment flag byte into \ the verge colour scheme \ \ ****************************************************************************** .vergeColour EQUB 0 \ * 0 = %000 = black right, black left, straight EQUB 0 \ * 1 = %001 = black right, black left, curve EQUB 1 \ * 2 = %010 = black right, red left, straight EQUB 1 \ * 3 = %011 = black right, red left, curve EQUB 1 \ * 4 = %100 = red right, black left, straight EQUB 1 \ * 5 = %101 = red right, black left, curve EQUB 1 \ * 6 = %110 = red right, red left, straight EQUB 1 \ * 7 = %111 = red right, red left, curve \ ****************************************************************************** \ \ Name: vergeScale \ Type: Variable \ Category: Track geometry \ Summary: Scale factors for the track verge width for different types of \ verge (larger value = smaller verge width) \ \ ****************************************************************************** .vergeScale EQUB 5 \ * 0 = %000 = black right, black left, straight EQUB 5 \ * 1 = %001 = black right, black left, curve EQUB 3 \ * 2 = %010 = black right, red left, straight EQUB 4 \ * 3 = %011 = black right, red left, curve EQUB 3 \ * 4 = %100 = red right, black left, straight EQUB 4 \ * 5 = %101 = red right, black left, curve EQUB 4 \ * 6 = %110 = red right, red left, straight EQUB 4 \ * 7 = %111 = red right, red left, curve EQUB &38, &38 \ These bytes appear to be unused \ ****************************************************************************** \ \ Name: staDrawByteTyre \ Type: Variable \ Category: Screen buffer \ Summary: Low address bytes of the STA instructions in the DRAW_BYTE macros, \ for use when drawing track lines around the tyres \ \ ------------------------------------------------------------------------------ \ \ This table contains the low byte offset of the address of the STA (P),Y \ instruction for the track line, which we convert into an RTS when drawing the \ track line up against the right tyre, so we stop in time. As the tyres are \ reflections of each other, we can also use this to calculate the starting \ point for the line that starts at the left tyre: see part 3 of DrawTrackView \ for details. \ \ ****************************************************************************** .staDrawByteTyre EQUB 8 * 17 + 15 \ Line 0 = LO(address) of STA (P),Y in DRAW_BYTE 34 EQUB 8 * 17 + 15 \ Line 1 = LO(address) of STA (P),Y in DRAW_BYTE 34 EQUB 8 * 17 + 15 \ Line 2 = LO(address) of STA (P),Y in DRAW_BYTE 34 EQUB 8 * 17 + 15 \ Line 3 = LO(address) of STA (P),Y in DRAW_BYTE 34 EQUB 9 * 17 + 15 \ Line 4 = LO(address) of STA (P),Y in DRAW_BYTE 35 EQUB 9 * 17 + 15 \ Line 5 = LO(address) of STA (P),Y in DRAW_BYTE 35 EQUB 9 * 17 + 15 \ Line 6 = LO(address) of STA (P),Y in DRAW_BYTE 35 EQUB 9 * 17 + 15 \ Line 7 = LO(address) of STA (P),Y in DRAW_BYTE 35 EQUB 9 * 17 + 15 \ Line 8 = LO(address) of STA (P),Y in DRAW_BYTE 35 EQUB 9 * 17 + 15 \ Line 9 = LO(address) of STA (P),Y in DRAW_BYTE 35 EQUB 9 * 17 + 15 \ Line 10 = LO(address) of STA (P),Y in DRAW_BYTE 35 EQUB 9 * 17 + 15 \ Line 11 = LO(address) of STA (P),Y in DRAW_BYTE 35 EQUB 9 * 17 + 15 \ Line 12 = LO(address) of STA (P),Y in DRAW_BYTE 35 EQUB 9 * 17 + 15 \ Line 13 = LO(address) of STA (P),Y in DRAW_BYTE 35 EQUB 9 * 17 + 15 \ Line 14 = LO(address) of STA (P),Y in DRAW_BYTE 35 EQUB 9 * 17 + 15 \ Line 15 = LO(address) of STA (P),Y in DRAW_BYTE 35 EQUB 9 * 17 + 15 \ Line 16 = LO(address) of STA (P),Y in DRAW_BYTE 35 EQUB 9 * 17 + 15 \ Line 17 = LO(address) of STA (P),Y in DRAW_BYTE 35 EQUB 9 * 17 + 15 \ Line 18 = LO(address) of STA (P),Y in DRAW_BYTE 35 EQUB 9 * 17 + 15 \ Line 19 = LO(address) of STA (P),Y in DRAW_BYTE 35 EQUB 9 * 17 + 15 \ Line 20 = LO(address) of STA (P),Y in DRAW_BYTE 35 EQUB 9 * 17 + 15 \ Line 21 = LO(address) of STA (P),Y in DRAW_BYTE 35 EQUB 10 * 17 + 15 \ Line 22 = LO(address) of STA (P),Y in DRAW_BYTE 36 EQUB 10 * 17 + 15 \ Line 23 = LO(address) of STA (P),Y in DRAW_BYTE 36 EQUB 10 * 17 + 15 \ Line 24 = LO(address) of STA (P),Y in DRAW_BYTE 36 EQUB 10 * 17 + 15 \ Line 25 = LO(address) of STA (P),Y in DRAW_BYTE 36 EQUB 10 * 17 + 15 \ Line 26 = LO(address) of STA (P),Y in DRAW_BYTE 36 EQUB 10 * 17 + 15 \ Line 27 = LO(address) of STA (P),Y in DRAW_BYTE 36 \ ****************************************************************************** \ \ Name: dashData1 \ Type: Variable \ Category: Screen buffer \ Summary: Contains code that gets moved into screen memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData1 SKIP 52 \ Populated with code from &7F98 to &7FCB \ ****************************************************************************** \ \ Name: ldaDrawByte \ Type: Variable \ Category: Screen buffer \ Summary: Low address bytes of the LDA #0 instructions in the DRAW_BYTE \ macros, for use when drawing track lines around the dashboard \ \ ****************************************************************************** .ldaDrawByte IF _ACORNSOFT OR _4TRACKS EQUB &1C \ Line 0 is unused and contains workspace noise ELIF _SUPERIOR OR _REVSPLUS EQUB &28 \ Line 0 is unused and contains workspace noise ENDIF EQUB 8 * 17 + 5 \ Line 1 = LO(address) of LDA #0 in DRAW_BYTE 34 EQUB 7 * 17 + 5 \ Line 2 = LO(address) of LDA #0 in DRAW_BYTE 33 EQUB 7 * 17 + 5 \ Line 3 = LO(address) of LDA #0 in DRAW_BYTE 33 EQUB 7 * 17 + 5 \ Line 4 = LO(address) of LDA #0 in DRAW_BYTE 33 EQUB 7 * 17 + 5 \ Line 5 = LO(address) of LDA #0 in DRAW_BYTE 33 EQUB 7 * 17 + 5 \ Line 6 = LO(address) of LDA #0 in DRAW_BYTE 33 EQUB 6 * 17 + 5 \ Line 7 = LO(address) of LDA #0 in DRAW_BYTE 32 EQUB 6 * 17 + 5 \ Line 8 = LO(address) of LDA #0 in DRAW_BYTE 32 EQUB 6 * 17 + 5 \ Line 9 = LO(address) of LDA #0 in DRAW_BYTE 32 EQUB 6 * 17 + 5 \ Line 10 = LO(address) of LDA #0 in DRAW_BYTE 32 EQUB 6 * 17 + 5 \ Line 11 = LO(address) of LDA #0 in DRAW_BYTE 32 EQUB 5 * 17 + 5 \ Line 12 = LO(address) of LDA #0 in DRAW_BYTE 31 EQUB 5 * 17 + 5 \ Line 13 = LO(address) of LDA #0 in DRAW_BYTE 31 EQUB 5 * 17 + 5 \ Line 14 = LO(address) of LDA #0 in DRAW_BYTE 31 EQUB 5 * 17 + 5 \ Line 15 = LO(address) of LDA #0 in DRAW_BYTE 31 EQUB 4 * 17 + 5 \ Line 16 = LO(address) of LDA #0 in DRAW_BYTE 30 EQUB 4 * 17 + 5 \ Line 17 = LO(address) of LDA #0 in DRAW_BYTE 30 EQUB 4 * 17 + 5 \ Line 18 = LO(address) of LDA #0 in DRAW_BYTE 30 EQUB 4 * 17 + 5 \ Line 19 = LO(address) of LDA #0 in DRAW_BYTE 30 EQUB 3 * 17 + 5 \ Line 20 = LO(address) of LDA #0 in DRAW_BYTE 29 EQUB 3 * 17 + 5 \ Line 21 = LO(address) of LDA #0 in DRAW_BYTE 29 EQUB 3 * 17 + 5 \ Line 22 = LO(address) of LDA #0 in DRAW_BYTE 29 EQUB 3 * 17 + 5 \ Line 23 = LO(address) of LDA #0 in DRAW_BYTE 29 EQUB 2 * 17 + 5 \ Line 24 = LO(address) of LDA #0 in DRAW_BYTE 28 EQUB 2 * 17 + 5 \ Line 25 = LO(address) of LDA #0 in DRAW_BYTE 28 EQUB 2 * 17 + 5 \ Line 26 = LO(address) of LDA #0 in DRAW_BYTE 28 EQUB 2 * 17 + 5 \ Line 27 = LO(address) of LDA #0 in DRAW_BYTE 28 EQUB 1 * 17 + 5 \ Line 28 = LO(address) of LDA #0 in DRAW_BYTE 27 EQUB 1 * 17 + 5 \ Line 29 = LO(address) of LDA #0 in DRAW_BYTE 27 EQUB 1 * 17 + 5 \ Line 30 = LO(address) of LDA #0 in DRAW_BYTE 27 EQUB 1 * 17 + 5 \ Line 31 = LO(address) of LDA #0 in DRAW_BYTE 27 EQUB 1 * 17 + 5 \ Line 32 = LO(address) of LDA #0 in DRAW_BYTE 27 EQUB 1 * 17 + 5 \ Line 33 = LO(address) of LDA #0 in DRAW_BYTE 27 EQUB 1 * 17 + 5 \ Line 34 = LO(address) of LDA #0 in DRAW_BYTE 27 EQUB 1 * 17 + 5 \ Line 35 = LO(address) of LDA #0 in DRAW_BYTE 27 EQUB 1 * 17 + 5 \ Line 36 = LO(address) of LDA #0 in DRAW_BYTE 27 EQUB 1 * 17 + 5 \ Line 37 = LO(address) of LDA #0 in DRAW_BYTE 27 EQUB 1 * 17 + 5 \ Line 38 = LO(address) of LDA #0 in DRAW_BYTE 27 EQUB 0 * 17 + 5 \ Line 39 = LO(address) of LDA #0 in DRAW_BYTE 26 EQUB 0 * 17 + 5 \ Line 40 = LO(address) of LDA #0 in DRAW_BYTE 26 EQUB 0 * 17 + 5 \ Line 41 = LO(address) of LDA #0 in DRAW_BYTE 26 EQUB 0 * 17 + 5 \ Line 42 = LO(address) of LDA #0 in DRAW_BYTE 26 EQUB 0 * 17 + 5 \ Line 43 = LO(address) of LDA #0 in DRAW_BYTE 26 \ ****************************************************************************** \ \ Name: vergeEdgeInOut \ Type: Variable \ Category: Drawing the track \ Summary: Table for mapping the verge tables to the outer and inner edges of \ the verge marks \ \ ****************************************************************************** .vergeEdgeInOut EQUB 16 \ leftVergeStart is an outer edge of the verge mark EQUB 0 \ leftTrackStart is an inner edge of the verge mark EQUB 0 \ rightVergeStart is an inner edge of the verge mark EQUB 16 \ rightGrassStart is an outer edge of the verge mark \ ****************************************************************************** \ \ Name: handPixels \ Type: Variable \ Category: Dashboard \ Summary: The number of pixels in the longest axis for the rev counter hand \ at various points in a half-quadrant \ Deep dive: Trigonometry \ \ ------------------------------------------------------------------------------ \ \ This table contains values that are used to calculate the coordinates of the \ end of the hand in the rev counter. \ \ The contents of the table are very close to the following (the values from \ the following calculation are shown in the comments below - they are close, \ but not quite a perfect match, so I haven't got this exactly right): \ \ FOR I%, 0, 21 \ EQUB INT(0.5 + 28 * COS((PI/4) * I% / 21)) \ NEXT \ \ This gives the length of the adjacent side of a right-angled triangle, with a \ hypotenuse of length 28, and an angle ranging from 0 to PI/4 (i.e. one \ eighth of a circle), split up into 21 points per eighth of a circle. \ \ In other words, if we have a clock whose centre is at the origin, then this \ table contains the x-coordinate of the end of a clock hand of length 28 as it \ moves from 3 o'clock to half past 4. \ \ ****************************************************************************** .handPixels EQUB 28 \ INT(0.5 + 28.00) = 28 EQUB 28 \ INT(0.5 + 27.98) = 28 EQUB 28 \ INT(0.5 + 27.92) = 28 EQUB 28 \ INT(0.5 + 27.82) = 28 EQUB 28 \ INT(0.5 + 27.69) = 28 EQUB 27 \ INT(0.5 + 27.51) = 28 (doesn't match) EQUB 27 \ INT(0.5 + 27.30) = 27 EQUB 27 \ INT(0.5 + 27.05) = 27 EQUB 27 \ INT(0.5 + 26.76) = 27 EQUB 26 \ INT(0.5 + 26.43) = 26 EQUB 26 \ INT(0.5 + 26.06) = 26 EQUB 26 \ INT(0.5 + 25.66) = 26 EQUB 25 \ INT(0.5 + 25.23) = 25 EQUB 25 \ INT(0.5 + 24.76) = 25 EQUB 24 \ INT(0.5 + 24.25) = 24 EQUB 24 \ INT(0.5 + 23.71) = 24 EQUB 23 \ INT(0.5 + 23.13) = 23 EQUB 22 \ INT(0.5 + 22.53) = 23 (doesn't match) EQUB 21 \ INT(0.5 + 21.89) = 22 (doesn't match) EQUB 20 \ INT(0.5 + 21.22) = 21 (doesn't match) EQUB 20 \ INT(0.5 + 20.53) = 21 (doesn't match) EQUB 20 \ INT(0.5 + 19.80) = 20 EQUB &81, &81 \ These bytes appear to be unused EQUB &81, &81 EQUB &81, &81 \ ****************************************************************************** \ \ Name: dashData2 \ Type: Variable \ Category: Screen buffer \ Summary: Contains code that gets moved into screen memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData2 SKIP 52 \ Populated with code from &7F64 to &7F97 \ ****************************************************************************** \ \ Name: staDrawByte \ Type: Variable \ Category: Screen buffer \ Summary: Low address bytes of the STA instructions in the DRAW_BYTE macros, \ for use when drawing track lines around the dashboard \ \ ****************************************************************************** .staDrawByte EQUB 3 * 17 + 15 \ Line 0 = LO(address) of STA (P),Y in DRAW_BYTE 3 EQUB 5 * 17 + 15 \ Line 1 = LO(address) of STA (P),Y in DRAW_BYTE 5 EQUB 6 * 17 + 15 \ Line 2 = LO(address) of STA (P),Y in DRAW_BYTE 6 EQUB 6 * 17 + 15 \ Line 3 = LO(address) of STA (P),Y in DRAW_BYTE 6 EQUB 6 * 17 + 15 \ Line 4 = LO(address) of STA (P),Y in DRAW_BYTE 6 EQUB 6 * 17 + 15 \ Line 5 = LO(address) of STA (P),Y in DRAW_BYTE 6 EQUB 6 * 17 + 15 \ Line 6 = LO(address) of STA (P),Y in DRAW_BYTE 6 EQUB 7 * 17 + 15 \ Line 7 = LO(address) of STA (P),Y in DRAW_BYTE 7 EQUB 7 * 17 + 15 \ Line 8 = LO(address) of STA (P),Y in DRAW_BYTE 7 EQUB 7 * 17 + 15 \ Line 9 = LO(address) of STA (P),Y in DRAW_BYTE 7 EQUB 7 * 17 + 15 \ Line 10 = LO(address) of STA (P),Y in DRAW_BYTE 7 EQUB 7 * 17 + 15 \ Line 11 = LO(address) of STA (P),Y in DRAW_BYTE 7 EQUB 8 * 17 + 15 \ Line 12 = LO(address) of STA (P),Y in DRAW_BYTE 8 EQUB 8 * 17 + 15 \ Line 13 = LO(address) of STA (P),Y in DRAW_BYTE 8 EQUB 8 * 17 + 15 \ Line 14 = LO(address) of STA (P),Y in DRAW_BYTE 8 EQUB 8 * 17 + 15 \ Line 15 = LO(address) of STA (P),Y in DRAW_BYTE 8 EQUB 9 * 17 + 15 \ Line 16 = LO(address) of STA (P),Y in DRAW_BYTE 9 EQUB 9 * 17 + 15 \ Line 17 = LO(address) of STA (P),Y in DRAW_BYTE 9 EQUB 9 * 17 + 15 \ Line 18 = LO(address) of STA (P),Y in DRAW_BYTE 9 EQUB 9 * 17 + 15 \ Line 19 = LO(address) of STA (P),Y in DRAW_BYTE 9 EQUB 10 * 17 + 15 \ Line 20 = LO(address) of STA (P),Y in DRAW_BYTE 10 EQUB 10 * 17 + 15 \ Line 21 = LO(address) of STA (P),Y in DRAW_BYTE 10 EQUB 10 * 17 + 15 \ Line 22 = LO(address) of STA (P),Y in DRAW_BYTE 10 EQUB 10 * 17 + 15 \ Line 23 = LO(address) of STA (P),Y in DRAW_BYTE 10 EQUB 11 * 17 + 15 \ Line 24 = LO(address) of STA (P),Y in DRAW_BYTE 11 EQUB 11 * 17 + 15 \ Line 25 = LO(address) of STA (P),Y in DRAW_BYTE 11 EQUB 11 * 17 + 15 \ Line 26 = LO(address) of STA (P),Y in DRAW_BYTE 11 EQUB 11 * 17 + 15 \ Line 27 = LO(address) of STA (P),Y in DRAW_BYTE 11 EQUB 12 * 17 + 15 \ Line 28 = LO(address) of STA (P),Y in DRAW_BYTE 12 EQUB 12 * 17 + 15 \ Line 29 = LO(address) of STA (P),Y in DRAW_BYTE 12 EQUB 12 * 17 + 15 \ Line 30 = LO(address) of STA (P),Y in DRAW_BYTE 12 EQUB 12 * 17 + 15 \ Line 31 = LO(address) of STA (P),Y in DRAW_BYTE 12 EQUB 12 * 17 + 15 \ Line 32 = LO(address) of STA (P),Y in DRAW_BYTE 12 EQUB 12 * 17 + 15 \ Line 33 = LO(address) of STA (P),Y in DRAW_BYTE 12 EQUB 12 * 17 + 15 \ Line 34 = LO(address) of STA (P),Y in DRAW_BYTE 12 EQUB 12 * 17 + 15 \ Line 35 = LO(address) of STA (P),Y in DRAW_BYTE 12 EQUB 12 * 17 + 15 \ Line 36 = LO(address) of STA (P),Y in DRAW_BYTE 12 EQUB 12 * 17 + 15 \ Line 37 = LO(address) of STA (P),Y in DRAW_BYTE 12 EQUB 12 * 17 + 15 \ Line 38 = LO(address) of STA (P),Y in DRAW_BYTE 12 EQUB 13 * 17 + 15 \ Line 39 = LO(address) of STA (P),Y in DRAW_BYTE 13 EQUB 13 * 17 + 15 \ Line 40 = LO(address) of STA (P),Y in DRAW_BYTE 13 EQUB 13 * 17 + 15 \ Line 41 = LO(address) of STA (P),Y in DRAW_BYTE 13 EQUB 13 * 17 + 15 \ Line 42 = LO(address) of STA (P),Y in DRAW_BYTE 13 EQUB 13 * 17 + 15 \ Line 43 = LO(address) of STA (P),Y in DRAW_BYTE 13 EQUB &28, &28 \ These bytes appear to be unused EQUB &00, &00 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 \ ****************************************************************************** \ \ Name: dashData3 \ Type: Variable \ Category: Screen buffer \ Summary: Contains code that gets moved into screen memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData3 SKIP 58 \ Populated with code from &7F2A to &7F63 \ ****************************************************************************** \ \ Name: DrawFence (Part 2 of 2) \ Type: Subroutine \ Category: Screen buffer \ Summary: Draw the fence that we crash into when running off the track \ \ ****************************************************************************** .fenc2 LDX #3 \ Set X = 3, to use as an index into the fencePixelsSky \ and fencePixelsGrass tables, so we draw pixel bytes \ repeatedly from these two tables to build up the fence \ effect .fenc3 CPY horizonLine \ If Y < horizonLine, then this pixel row is below the BCC fenc4 \ horizon, so jump to fenc4 to draw the fence with green \ grass behind it LDA fencePixelsSky,X \ Otherwise this pixel row is above the horizon, so set \ A to the correct pixel byte for the fence with blue \ sky behind it BNE fenc5 \ Jump to fenc5 (this BNE is effectively a JMP as A is \ never zero) .fenc4 LDA fencePixelsGrass,X \ Set A to the correct pixel byte for the fence with \ green grass behind it .fenc5 STA (P),Y \ Store A in the dash data block at (Q P), to draw this \ four-pixel part of the fence in the track view STA tyreRightEdge,Y \ Store A in the tyreRightEdge and dashRightEdge STA dashRightEdge,Y \ entries for this row, so the drawing routines can wrap \ the fence correctly around the dashboard and tyres DEX \ Decrement X to point to the next pixel byte in the \ fence pixel byte tables BPL fenc6 \ If we just decremented X to -1, set it back to 3, so LDX #3 \ X goes 3, 2, 1, 0, then 3, 2, 1, 0, and so on .fenc6 DEY \ Decrement the byte counter in Y to point to the next \ byte in the dash data block CPY U \ If Y <> U then we have not yet drawn the fence in all BNE fenc3 \ the bytes in the dash data block (as U contains the \ dashDataOffset for this block, which contains the \ offset of the last byte that we need to fill), so loop \ back to draw the next byte of the fence \ If we get here then we have drawn fence through the \ whole dash data block, so now we move on to the next \ block by updating the counter in T and pointing (Q P) \ to the next dash data block INC T \ Increment the loop counter to point to the next dash \ data block LDA P \ Set (Q P) = (Q P) + &80 EOR #&80 \ STA P \ starting with the low bytes \ \ We can do the addition more efficiently by using EOR \ to flip between &xx00 and &xx80, as the dash data \ blocks always start at these addresses BMI fenc7 \ We then increment the high byte, but only if the EOR INC Q \ set the low byte to &00 rather than &80 (if we just \ set it to the latter, the BMI will skip the INC) .fenc7 JMP fenc1 \ Loop back to part 1 to draw the fence in the next dash \ data block EQUB 0 \ This byte appears to be unused \ ****************************************************************************** \ \ Name: token18 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 18 \ Deep dive: Text tokens \ \ ****************************************************************************** .token18 EQUS " 5" \ Print " 5" EQUB 255 \ End token EQUB &81 \ This byte appears to be unused \ ****************************************************************************** \ \ Name: dashData4 \ Type: Variable \ Category: Screen buffer \ Summary: Contains code that gets moved into screen memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData4 SKIP 76 \ Populated with code from &7EDE to &7F29 \ ****************************************************************************** \ \ Name: PrintDriverName \ Type: Subroutine \ Category: Text \ Summary: Print a driver's name \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ (Y A) Address of 12-character driver name \ \ ****************************************************************************** .PrintDriverName STY S \ Set (S R) = (Y A) STA R LDY #0 \ Set a character counter in Y .name1 LDA (R),Y \ Set A to the Y-th character from (S R) JSR PrintCharacter \ Print the character in A INY \ Increment the character counter CPY #12 \ Loop back to print the next character until we have BNE name1 \ printed all 12 characters RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: CheckRestartKeys \ Type: Subroutine \ Category: Keyboard \ Summary: If the restart keys are being pressed, restart the game \ \ ------------------------------------------------------------------------------ \ \ Other entry points: \ \ RestartGame Restart the game, putting the C flag into bit 7 of \ pressingShiftArrow \ \ ****************************************************************************** .CheckRestartKeys LDX #&FF \ Scan the keyboard to see if SHIFT is being pressed JSR ScanKeyboard BNE rest1 \ If SHIFT is not being pressed, jump to rest1 LDX #&86 \ Scan the keyboard to see if right arrow is being JSR ScanKeyboard \ pressed (if it is this will also set the C flag) BNE rest1 \ If right arrow is not being pressed, jump to rest1 BIT pressingShiftArrow \ If bit 7 of pressingShiftArrow is set, then we are BMI rest2 \ still pressing Shift-arrow from a previous restart, so \ jump to rest2 to return from the subroutine without \ anything doing \ If we get here then SHIFT-arrow is being pressed and \ bit 7 of pressingShiftArrow is clear, so this is a \ new pressing SHIFT-arrow, so we fall through into \ RestartGame with the C flag set to restart the game \ and set bit 7 of pressingShiftArrow .RestartGame LDX startingStack \ Set the stack pointer to the value it had when the TXS \ game started, which clears any stored addresses put on \ the stack by the code we are now exiting from ROR pressingShiftArrow \ Shift the C flag into bit 7 of pressingShiftArrow JMP MainLoop \ Jump to the start of the main game loop to restart the \ game .rest1 LSR pressingShiftArrow \ Clear bit 7 of pressingShiftArrow to indicate that we \ are no longer pressing SHIFT-arrow .rest2 RTS \ Return from the subroutine EQUB &00, &00 \ These bytes appear to be unused \ ****************************************************************************** \ \ Name: token19 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 19 \ Deep dive: Text tokens \ \ ****************************************************************************** .token19 EQUS "10" \ Print "10" EQUB 255 \ End token \ ****************************************************************************** \ \ Name: dashData5 \ Type: Variable \ Category: Screen buffer \ Summary: Contains code that gets moved into screen memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData5 SKIP 77 \ Populated with code from &7E91 to &7EDD \ ****************************************************************************** \ \ Name: GetNumberFromText \ Type: Subroutine \ Category: Text \ Summary: Convert a two-digit string into a number \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ T The first digit of the number, as text \ \ U The second digit of the number, as text \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ A The numerical value of the number \ \ C flag The status of the conversion: \ \ * Clear if the string is a valid number and A <= 40 \ \ * Set if string is not a valid number, or A > 40 \ \ ****************************************************************************** .GetNumberFromText LDA T \ Set A to the character containing the first digit CMP #' ' \ If the first digit is not a space, skip the following BNE tnum1 \ instruction LDA #'0' \ The first digit is a space, so convert it to a "0" .tnum1 SEC \ Subtract the ASCII value for "0" to get the numerical SBC #'0' \ value of the first digit into A CMP #10 \ If the value of the first digit is greater than 10, BCS tnum2 \ then this is not a valid number, so jump to tnum2 to \ return from the subroutine with the C flag set, to \ indicate an error STA T \ Set T = the value of the first digit LDX U \ Set X to the character containing the second digit CPX #' ' \ If the second digit is a space, then jump to tnum2 to CLC \ return from the subroutine with the value of the first BEQ tnum2 \ digit in A and the C flag clear, to indicate success ASL A \ Set T = (A << 2 + T) << 1 ASL A \ = (A * 4 + A) * 2 ADC T \ = 10 * A ASL A \ STA T \ So T contains 10 * the numerical value of the first \ digit TXA \ Set A to the character containing the second digit SEC \ Subtract the ASCII value for "0" to get the numerical SBC #'0' \ value of the second digit CMP #10 \ If the value of the second digit is greater than 10, BCS tnum2 \ then this is not a valid number, so jump to tnum2 to \ return from the subroutine with the C flag set, to \ indicate an error ADC T \ Set A = A + T \ = the numerical value of the second digit \ + 10 * the numerical value of the first digit \ \ which is the numerical value of the two-digit string CMP #41 \ If A < 41, clear the C flag, otherwise set it .tnum2 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: startDialLo \ Type: Variable \ Category: Drawing pixels \ Summary: The low byte of the screen address of the start of the dial hand \ on the rev counter \ \ ****************************************************************************** .startDialLo EQUB &66 \ Quadrant 0 (12:00 to 3:00) = &7566 EQUB &67 \ Quadrant 1 (3:00 to 6:00) = &7567 EQUB &5F \ Quadrant 2 (6:00 to 9:00) = &755F EQUB &5E \ Quadrant 3 (9:00 to 12:00) = &755E \ ****************************************************************************** \ \ Name: token20 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 20 \ Deep dive: Text tokens \ \ ****************************************************************************** .token20 EQUS "20" \ Print "20" EQUB 255 \ End token \ ****************************************************************************** \ \ Name: dashData6 \ Type: Variable \ Category: Screen buffer \ Summary: Contains code that gets moved into screen memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData6 SKIP 77 \ Populated with code from &7E44 to &7E90 \ ****************************************************************************** \ \ Name: leftDashPixels \ Type: Variable \ Category: Dashboard \ Summary: Pixels along the left edge of the dashboard \ Deep dive: Drawing around the dashboard \ \ ------------------------------------------------------------------------------ \ \ Contains a pixel byte for the white border (colour 2) along the left edge of \ the dashboard. \ \ There is a byte for each track line from 43 (the track line at the top of the \ dashboard) down to 3 (the lowest track line, just above where the wing mirror \ joins the car body). Lines 0 to 2 are not used. \ \ Each pixel is a colour 2 pixel, so the high nibble contains a 1 and the low \ nibble contains a 0, to give colour %10. Colour 2 is mapped to white at this \ point of the custom screen. \ \ ****************************************************************************** .leftDashPixels EQUB %00000000 \ Line 0 EQUB %00000000 \ Line 1 EQUB %11110000 \ Line 2 EQUB %01110000 \ Line 3 EQUB %00110000 \ Line 4 EQUB %00010000 \ Line 5 EQUB %00000000 \ Line 6 EQUB %01110000 \ Line 7 EQUB %01110000 \ Line 8 EQUB %00110000 \ Line 9 EQUB %00010000 \ Line 10 EQUB %00000000 \ Line 11 EQUB %01110000 \ Line 12 EQUB %00110000 \ Line 13 EQUB %00010000 \ Line 14 EQUB %00000000 \ Line 15 EQUB %01110000 \ Line 16 EQUB %00110000 \ Line 17 EQUB %00010000 \ Line 18 EQUB %00000000 \ Line 19 EQUB %01110000 \ Line 20 EQUB %00110000 \ Line 21 EQUB %00010000 \ Line 22 EQUB %00000000 \ Line 23 EQUB %01110000 \ Line 24 EQUB %00110000 \ Line 25 EQUB %00010000 \ Line 26 EQUB %00000000 \ Line 27 EQUB %01110000 \ Line 28 EQUB %01000000 \ Line 29 EQUB %00110000 \ Line 30 EQUB %00100000 \ Line 31 EQUB %00110000 \ Line 32 EQUB %00010000 \ Line 33 EQUB %00010000 \ Line 34 EQUB %00010000 \ Line 35 EQUB %00000000 \ Line 36 EQUB %00000000 \ Line 37 EQUB %00000000 \ Line 38 EQUB %01000000 \ Line 39 EQUB %01110000 \ Line 40 EQUB %01000000 \ Line 41 EQUB %00110000 \ Line 42 EQUB %00110000 \ Line 43 \ ****************************************************************************** \ \ Name: pixelsToLeft \ Type: Variable \ Category: Drawing pixels \ Summary: Pixel byte with all the pixels to the left of position X set \ \ ****************************************************************************** .pixelsToLeft EQUB %00000000 EQUB %10001000 EQUB %11001100 EQUB %11101110 EQUB &81, &81 \ These bytes appear to be unused EQUB &81, &81 EQUB &81, &81 EQUB &81 \ ****************************************************************************** \ \ Name: dashData7 \ Type: Variable \ Category: Screen buffer \ Summary: Contains code that gets moved into screen memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData7 SKIP 73 \ Populated with code from &7DFB to &7E43 \ ****************************************************************************** \ \ Name: rightDashPixels \ Type: Variable \ Category: Dashboard \ Summary: Pixels along the right edge of the dashboard \ Deep dive: Drawing around the dashboard \ \ ------------------------------------------------------------------------------ \ \ Contains a pixel byte for the white border (colour 2) along the right edge of \ the dashboard. \ \ There is a byte for each track line from 43 (the track line at the top of the \ dashboard) down to 3 (the lowest track line, just above where the wing mirror \ joins the car body). Lines 0 to 2 are not used. \ \ Each pixel is a colour 2 pixel, so the high nibble contains a 1 and the low \ nibble contains a 0, to give colour %10. Colour 2 is mapped to white at this \ point of the custom screen. \ \ ****************************************************************************** .rightDashPixels EQUB %00000000 \ Line 0 EQUB %00000000 \ Line 1 EQUB %11110000 \ Line 2 EQUB %11100000 \ Line 3 EQUB %11000000 \ Line 4 EQUB %10000000 \ Line 5 EQUB %00000000 \ Line 6 EQUB %11100000 \ Line 7 EQUB %11100000 \ Line 8 EQUB %11000000 \ Line 9 EQUB %10000000 \ Line 10 EQUB %00000000 \ Line 11 EQUB %11100000 \ Line 12 EQUB %11000000 \ Line 13 EQUB %10000000 \ Line 14 EQUB %00000000 \ Line 15 EQUB %11100000 \ Line 16 EQUB %11000000 \ Line 17 EQUB %10000000 \ Line 18 EQUB %00000000 \ Line 19 EQUB %11100000 \ Line 20 EQUB %11000000 \ Line 21 EQUB %10000000 \ Line 22 EQUB %00000000 \ Line 23 EQUB %11100000 \ Line 24 EQUB %11000000 \ Line 25 EQUB %10000000 \ Line 26 EQUB %00000000 \ Line 27 EQUB %11100000 \ Line 28 EQUB %00100000 \ Line 29 EQUB %11000000 \ Line 30 EQUB %01000000 \ Line 31 EQUB %11000000 \ Line 32 EQUB %10000000 \ Line 33 EQUB %10000000 \ Line 34 EQUB %10000000 \ Line 35 EQUB %00000000 \ Line 36 EQUB %00000000 \ Line 37 EQUB %00000000 \ Line 38 EQUB %00100000 \ Line 39 EQUB %11100000 \ Line 40 EQUB %00100000 \ Line 41 EQUB %11000000 \ Line 42 EQUB %11000000 \ Line 43 \ ****************************************************************************** \ \ Name: pixelsEdgeRight \ Type: Variable \ Category: Drawing pixels \ Summary: Pixel byte with all the pixels to the right of position X set, \ plus pixel X \ \ ****************************************************************************** .pixelsEdgeRight EQUB %11111111 EQUB %01110111 EQUB %00110011 EQUB %00010001 \ ****************************************************************************** \ \ Name: token11 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 11 \ Deep dive: Text tokens \ \ ****************************************************************************** .token11 EQUS "ENTER " \ Print "ENTER " EQUB 255 \ End token EQUB &81, &81 \ These bytes appear to be unused EQUB &81, &81 EQUB &81 \ ****************************************************************************** \ \ Name: dashData8 \ Type: Variable \ Category: Screen buffer \ Summary: Contains code that gets moved into screen memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData8 SKIP 68 \ Populated with code from &7DB7 to &7DFA \ ****************************************************************************** \ \ Name: Absolute8Bit \ Type: Subroutine \ Category: Maths (Arithmetic) \ Summary: Calculate the absolute value (modulus) of an 8-bit number \ \ ------------------------------------------------------------------------------ \ \ This routine returns |A|. \ \ It can also return A * abs(n), where A is given the sign of n. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ A The number to make positive \ \ N flag Controls the sign to be applied: \ \ * If we want to calculate |A|, do an LDA or equivalent \ before calling the routine \ \ * If we want to calculate A * abs(n), do a BIT n \ before calling the routine \ \ * If we want to set the sign of A, then call with: \ \ * N flag clear to calculate A * 1 \ \ * N flag set to calculate A * -1 \ \ ****************************************************************************** .Absolute8Bit BPL aval1 \ If A is positive then it already contains its absolute \ value, so jump to aval1 to return from the subroutine EOR #&FF \ Negate the value in A using two's complement, as the CLC \ following is true when A is negative: ADC #1 \ \ |A| = -A \ = ~A + 1 .aval1 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: paletteSection2 \ Type: Variable \ Category: Screen mode \ Summary: Colour palette for screen section 2 in the custom screen mode \ (part of the mode 5 portion) \ Deep dive: Hidden secrets of the custom screen mode \ \ ------------------------------------------------------------------------------ \ \ Palette data is given as a set of bytes, with each byte mapping a logical \ colour to a physical one. In each byte, the logical colour is given in bits \ 4-7 and the physical colour in bits 0-3. See p.379 of the Advanced User Guide \ for details of how palette mapping works, as in modes 4 and 5 we have to do \ multiple palette commands to change the colours correctly, and the physical \ colour value is EOR'd with 7, just to make things even more confusing. \ \ Each of these mappings requires six calls to SHEILA &21 - see p.379 of the \ Advanced User Guide for an explanation. \ \ ****************************************************************************** .paletteSection2 EQUB &07, &17 \ Map logical colour 0 to physical colour 0 (black) EQUB &47, &57 EQUB &23, &33 \ Map logical colour 1 to physical colour 4 (blue) EQUB &63, &73 EQUB &80, &90 \ Map logical colour 2 to physical colour 7 (white) EQUB &C0, &D0 EQUB &A5, &B5 \ Map logical colour 3 to physical colour 2 (green) EQUB &E5, &F5 \ ****************************************************************************** \ \ Name: paletteSection0 \ Type: Variable \ Category: Screen mode \ Summary: Colour palette for screen section 0 in the custom screen mode (the \ mode 4 portion) \ Deep dive: Hidden secrets of the custom screen mode \ \ ****************************************************************************** .paletteSection0 EQUB &03, &13 \ Map logical colour 0 to physical colour 4 (blue) EQUB &23, &33 EQUB &43, &53 EQUB &63, &73 EQUB &84, &94 \ Map logical colour 1 to physical colour 3 (yellow) EQUB &A4, &B4 EQUB &C4, &D4 EQUB &E4, &F4 \ ****************************************************************************** \ \ Name: paletteSection3 \ Type: Variable \ Category: Screen mode \ Summary: Colour palette for screen section 3 in the custom screen mode \ (part of the mode 5 portion) \ Deep dive: Hidden secrets of the custom screen mode \ \ ****************************************************************************** .paletteSection3 EQUB &26, &36 \ Map logical colour 1 to physical colour 1 (red) EQUB &66, &76 \ ****************************************************************************** \ \ Name: paletteSection4 \ Type: Variable \ Category: Screen mode \ Summary: Colour palette for screen section 4 in the custom screen mode \ (part of the mode 5 portion) \ Deep dive: Hidden secrets of the custom screen mode \ \ ****************************************************************************** .paletteSection4 EQUB &A1, &B1 \ Map logical colour 3 to physical colour 6 (cyan) EQUB &E1, &F1 \ ****************************************************************************** \ \ Name: token25 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 25 \ Deep dive: Text tokens \ \ ****************************************************************************** .token25 EQUB 31, 13, 18 \ Move text cursor to column 13, row 18 EQUS "front" \ Print "front" EQUB 160 + 2 \ Print 2 spaces EQUB 133 \ Set foreground colour to magenta alphanumeric EQUB 200 + 16 \ Print token 16 (" > ") EQUB 255 \ End token EQUB &81, &81 \ These bytes appear to be unused EQUB &81, &81 \ ****************************************************************************** \ \ Name: dashData9 \ Type: Variable \ Category: Screen buffer \ Summary: Contains code that gets moved into screen memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData9 SKIP 64 \ Populated with code from &7D77 to &7DB6 \ ****************************************************************************** \ \ Name: WaitForSpace \ Type: Subroutine \ Category: Keyboard \ Summary: Print a prompt, wait for the SPACE key to be released, and wait \ for SPACE to be pressed \ \ ****************************************************************************** .WaitForSpace LDA #0 \ Set A = 0 so WaitForSpaceReturn waits for SPACE to be \ pressed \ Fall through into WaitForSpaceReturn to print the \ prompt, wait for the SPACE key to be released, and \ wait for SPACE to be pressed \ ****************************************************************************** \ \ Name: WaitForSpaceReturn \ Type: Subroutine \ Category: Keyboard \ Summary: Print a prompt, wait for the SPACE key to be released, and wait \ for either SPACE or RETURN to be pressed \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ A Determines the key to wait for: \ \ * Bit 7 clear = wait for SPACE to be pressed \ \ * Bit 7 set = wait for SPACE or RETURN to be pressed \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ G If bit 7 was set on entry, then it is cleared if RETURN \ was pressed, but remains set if SPACE was pressed \ \ ****************************************************************************** .WaitForSpaceReturn STA G \ Store A in G so we can check the value of bit 7 below LDX #30 \ Print token 30 ("PRESS SPACE BAR TO CONTINUE" in cyan JSR PrintToken \ at column 5, row 24) .wait1 LDX #&9D \ Scan the keyboard to see if SPACE is being pressed JSR ScanKeyboard BEQ wait1 \ If SPACE is being pressed, loop back to wait1 until \ it is released .wait2 LDX #&9D \ Scan the keyboard to see if SPACE is being pressed JSR ScanKeyboard BEQ wait3 \ If SPACE is being pressed, jump to wait3 to return \ from the subroutine JSR CheckRestartKeys \ Check whether the restart keys are being pressed, and \ if they are, restart the game (the restart keys are \ SHIFT and right arrow) BIT G \ If bit 7 of G is clear, jump back to wait2 to wait for BPL wait2 \ SPACE to be pressed LDX #&B6 \ Scan the keyboard to see if RETURN is being pressed JSR ScanKeyboard BNE wait2 \ If RETURN is not being pressed, jump back to wait2 to \ wait for RETURN is being pressed LSR G \ Clear bit 7 of G .wait3 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: pixelByte \ Type: Variable \ Category: Drawing pixels \ Summary: A table of pixel bytes with individual pixels set \ \ ****************************************************************************** .pixelByte EQUB %10000000 \ Pixel byte with the first pixel set to colour 2 EQUB %01000000 \ Pixel byte with the second pixel set to colour 2 EQUB %00100000 \ Pixel byte with the third pixel set to colour 2 EQUB %00010000 \ Pixel byte with the fourth pixel set to colour 2 EQUB %00000000 \ Pixel byte with the first pixel set to colour 0 EQUB %00000000 \ Pixel byte with the second pixel set to colour 0 EQUB %00000000 \ Pixel byte with the third pixel set to colour 0 EQUB %00000000 \ Pixel byte with the fourth pixel set to colour 0 \ ****************************************************************************** \ \ Name: token8 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 8 \ Deep dive: Text tokens \ \ ****************************************************************************** .token8 EQUS "Amateur" \ Print "Amateur" EQUB 255 \ End token \ ****************************************************************************** \ \ Name: token51 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 51 \ Deep dive: Text tokens \ \ ****************************************************************************** .token51 EQUS " POINTS" \ Print " POINTS" EQUB 255 \ End token EQUB &81, &81 \ These bytes appear to be unused EQUB &81, &81 \ ****************************************************************************** \ \ Name: dashData10 \ Type: Variable \ Category: Screen buffer \ Summary: Contains code that gets moved into screen memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData10 SKIP 60 \ Populated with code from &7D3B to &7D76 \ ****************************************************************************** \ \ Name: objectTop \ Type: Variable \ Category: 3D objects \ Summary: Scaffold measurements for the top of each object part \ Deep dive: Object definitions \ Scaling objects with scaffolds \ \ ------------------------------------------------------------------------------ \ \ Entries contain indexes into the scaledScaffold table. n + 8 points to the \ negative value of n (as scaledScaffold+8 is filled with the negative of \ scaledScaffold). \ \ ****************************************************************************** .objectTop EQUB 7 + 8 \ Object 0, Part 0: Scaffolds: (-7, -6, -1, -2) \ Coordinates: (-4, -5, -22, -18) EQUB 7 + 8 \ Object 0, Part 1: Scaffolds: (-7, -6, 2, 1) \ Coordinates: (-4, -5, 18, 22) EQUB 6 + 8 \ Object 0, Part 2: Scaffolds: (-6, -3, -0, -4) \ Coordinates: (-5, -17, -24, -16) EQUB 6 + 8 \ Object 0, Part 3: Scaffolds: (-6, -3, 4, 0) \ Coordinates: (-5, -17, 16, 24) EQUB 5 + 8 \ Object 0, Part 4: Scaffolds: (-5, -3, -2, 2) \ Coordinates: (-8, -17, -18, 18) EQUB 5 \ Object 1, Part 0: Scaffolds: (5, 6, -5, 5) \ Coordinates: (5, 2, -5, 5) EQUB 6 \ Object 1, Part 1: Scaffolds: (6, -7, -3, 3) \ Coordinates: (2, -1, -8, 8) EQUB 7 + 8 \ Object 1, Part 2: Scaffolds: (-7, -5, -1, 1) \ Coordinates: (-1, -5, -12, 12) EQUB 4 \ Object 1, Part 3: Scaffolds: (4, 5, -6, 6) \ Coordinates: (6, 5, -2, 2) EQUB 6 + 8 \ Object 2, Part 0: Scaffolds: (-6, -5, -0, -4) \ Coordinates: (-3, -5, -26, -16) EQUB 0 \ Extra edges: Scaffolds: 0, 4 \ Coordinates: 26, 16 EQUB 5 + 8 \ Object 2, Part 2: Scaffolds: (-5, -3, -0, 0) \ Coordinates: (-5, -17, -26, 26) EQUB 7 + 8 \ Object 2, Part 3: Scaffolds: (-7, -6, -1, -2) \ Coordinates: (-2, -3, -24, -18) EQUB 7 + 8 \ Object 2, Part 4: Scaffolds: (-7, -6, 2, 1) \ Coordinates: (-2, -3, 18, 24) EQUB 2 \ Object 3, Part 0: Scaffolds: (2, 3, -0, 0) \ Coordinates: (6, 4, -16, 16) EQUB 3 \ Object 3, Part 1: Scaffolds: (3, 4, -0, 0) \ Coordinates: (4, 3, -16, 16) EQUB 4 \ Object 3, Part 2: Scaffolds: (4, -1, -5, 5) \ Coordinates: (3, -10, -1, 1) EQUB 1 + 8 \ Object 3, Part 3: Scaffolds: (-1, -0, -5, 5) \ Coordinates: (-10, -16, -1, 1) EQUB 6 \ Object 4, Part 0: Scaffolds: (6, -7, -4, 4) \ Coordinates: (3, -1, -6, 6) EQUB 7 + 8 \ Object 4, Part 1: Scaffolds: (-7, -5, -3, 3) \ Coordinates: (-1, -5, -12, 12) EQUB 6 + 8 \ Object 4, Part 2: Scaffolds: (-6, -5, -0, -2) \ Coordinates: (-3, -5, -26, -16) EQUB 0 \ Extra edges: Scaffolds: 0, 4 \ Coordinates: 26, 16 EQUB 5 + 8 \ Object 4, Part 4: Scaffolds: (-5, -1, -0, 0) \ Coordinates: (-5, -17, -26, 26) EQUB 7 \ Object 4, Part 5: Scaffolds: (7, -5, -7, 7) \ Coordinates: (1, -5, -1, 1) EQUB 4 \ Object 4, Part 6: Scaffolds: (4, 6, -2, 2) \ Coordinates: (6, 3, -16, 16) EQUB 2 + 8 \ Object 5, Part 0: Scaffolds: (-2, -1, -0, 0) \ Coordinates: (-3, -17, -26, 26) EQUB 0 \ Object 6, Part 0: Scaffolds: (0, 2, -1, 1) \ Coordinates: (16, 1, -10, 10) EQUB 1 \ Object 7, Part 0: Scaffolds: (1, -4, -0, 0) \ Coordinates: (20, -8, -28, 28) EQUB 4 + 8 \ Object 7, Part 1: Scaffolds: (-4, -2, -1, -3) \ Coordinates: (-8, -18, -20, -16) EQUB 4 + 8 \ Object 7, Part 2: Scaffolds: (-4, -2, 3, 1) \ Coordinates: (-8, -18, 16, 20) EQUB 2 \ Object 8, Part 0: Scaffolds: (2, -0, -3, 3) \ Coordinates: (3, -18, -2, 2) EQUB 1 \ Object 8, Part 1: Scaffolds: (1, 2, -3, 1) \ Coordinates: (16, 3, -2, 16) EQUB 1 \ Object 9, Part 0: Scaffolds: (1, -2, -0, 0) \ Coordinates: (12, -10, -16, 16) EQUB 2 + 8 \ Object 9, Part 1: Scaffolds: (-2, -0, -3, 3) \ Coordinates: (-10, -16, -3, 3) EQUB 3 \ Object 10, Part 0: Scaffolds: (3, 4, -0, 0) \ Coordinates: (4, 1, -10, 10) EQUB 4 \ Object 10, Part 1: Scaffolds: (4, -2, -0, -1) \ Coordinates: (1, -6, -10, -9) EQUB 0 \ Object 10, Part 2: Scaffolds: (0, 3, 1, 0) \ Coordinates: (10, 4, 9, 10) EQUB 1 \ Object 11, Part 0: Scaffolds: (1, 3, -0, 1) \ Coordinates: (8, 5, -10, 8) EQUB 3 \ Object 11, Part 1: Scaffolds: (3, -2, -0, -1) \ Coordinates: (5, -6, -10, -8) EQUB 1 \ Object 12, Part 0: Scaffolds: (1, 3, -1, 0) \ Coordinates: (8, 5, -8, 10) EQUB 3 \ Object 12, Part 1: Scaffolds: (3, -2, 1, 0) \ Coordinates: (5, -6, 8, 10) \ ****************************************************************************** \ \ Name: leftTyrePixels \ Type: Variable \ Category: Dashboard \ Summary: Pixels along the edge of the left tyre \ Deep dive: Drawing around the dashboard \ \ ------------------------------------------------------------------------------ \ \ Contains a pixel byte for the white border (colour 2) along the edge of the \ left tyre. \ \ The tyreEdgeIndex table maps track line numbers to entries in this table. \ \ Each pixel is a colour 2 pixel, so the high nibble contains a 1 and the low \ nibble contains a 0, to give colour %10. Colour 2 is mapped to white at this \ point of the custom screen. \ \ ****************************************************************************** .leftTyrePixels EQUB %00000000 EQUB %10000000 EQUB %11000000 EQUB %01000000 EQUB %01100000 EQUB %11100000 EQUB %00100000 \ ****************************************************************************** \ \ Name: token31 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 31 \ Deep dive: Text tokens \ \ ****************************************************************************** .token31 EQUS " " \ Print " " EQUB 156 \ Set background colour to black EQUB 134, 157 \ Set background colour (configurable, default is cyan) EQUB 132 \ Set foreground colour (configurable, default is blue \ alphanumeric EQUB 255 \ End token \ ****************************************************************************** \ \ Name: token3 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 3 \ Deep dive: Text tokens \ \ ****************************************************************************** .token3 EQUS "ACCUMULATED" \ Print "ACCUMULATED" EQUB 200 + 51 \ Print token 51 (" POINTS") EQUB 255 \ End token EQUB &81, &81 \ These bytes appear to be unused EQUB &81, &81 \ ****************************************************************************** \ \ Name: dashData11 \ Type: Variable \ Category: Screen buffer \ Summary: Contains code that gets moved into screen memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData11 SKIP 56 \ Populated with code from &7D03 to &7D3A \ ****************************************************************************** \ \ Name: objectBottom \ Type: Variable \ Category: 3D objects \ Summary: Scaffold measurements for the bottom of each object part \ Deep dive: Object definitions \ Scaling objects with scaffolds \ \ ------------------------------------------------------------------------------ \ \ Entries contain indexes into the scaledScaffold table. n + 8 points to the \ negative value of n (as scaledScaffold+8 is filled with the negative of \ scaledScaffold). \ \ ****************************************************************************** .objectBottom EQUB 6 + 8 \ Object 0, Part 0: Scaffolds: (-7, -6, -1, -2) \ Coordinates: (-4, -5, -22, -18) EQUB 6 + 8 \ Object 0, Part 1: Scaffolds: (-7, -6, 2, 1) \ Coordinates: (-4, -5, 18, 22) EQUB 3 + 8 \ Object 0, Part 2: Scaffolds: (-6, -3, -0, -4) \ Coordinates: (-5, -17, -24, -16) EQUB 3 + 8 \ Object 0, Part 3: Scaffolds: (-6, -3, 4, 0) \ Coordinates: (-5, -17, 16, 24) EQUB 3 + 8 \ Object 0, Part 4: Scaffolds: (-5, -3, -2, 2) \ Coordinates: (-8, -17, -18, 18) EQUB 6 \ Object 1, Part 0: Scaffolds: (5, 6, -5, 5) \ Coordinates: (5, 2, -5, 5) EQUB 7 + 8 \ Object 1, Part 1: Scaffolds: (6, -7, -3, 3) \ Coordinates: (2, -1, -8, 8) EQUB 5 + 8 \ Object 1, Part 2: Scaffolds: (-7, -5, -1, 1) \ Coordinates: (-1, -5, -12, 12) EQUB 5 \ Object 1, Part 3: Scaffolds: (4, 5, -6, 6) \ Coordinates: (6, 5, -2, 2) EQUB 5 + 8 \ Object 2, Part 0: Scaffolds: (-6, -5, -0, -4) \ Coordinates: (-3, -5, -26, -16) EQUB 7 \ Extra edges: Scaffolds: 0, 4 \ Coordinates: 26, 16 EQUB 3 + 8 \ Object 2, Part 2: Scaffolds: (-5, -3, -0, 0) \ Coordinates: (-5, -17, -26, 26) EQUB 6 + 8 \ Object 2, Part 3: Scaffolds: (-7, -6, -1, -2) \ Coordinates: (-2, -3, -24, -18) EQUB 6 + 8 \ Object 2, Part 4: Scaffolds: (-7, -6, 2, 1) \ Coordinates: (-2, -3, 18, 24) EQUB 3 \ Object 3, Part 0: Scaffolds: (2, 3, -0, 0) \ Coordinates: (6, 4, -16, 16) EQUB 4 \ Object 3, Part 1: Scaffolds: (3, 4, -0, 0) \ Coordinates: (4, 3, -16, 16) EQUB 1 + 8 \ Object 3, Part 2: Scaffolds: (4, -1, -5, 5) \ Coordinates: (3, -10, -1, 1) EQUB 0 + 8 \ Object 3, Part 3: Scaffolds: (-1, -0, -5, 5) \ Coordinates: (-10, -16, -1, 1) EQUB 7 + 8 \ Object 4, Part 0: Scaffolds: (6, -7, -4, 4) \ Coordinates: (3, -1, -6, 6) EQUB 5 + 8 \ Object 4, Part 1: Scaffolds: (-7, -5, -3, 3) \ Coordinates: (-1, -5, -12, 12) EQUB 5 + 8 \ Object 4, Part 2: Scaffolds: (-6, -5, -0, -2) \ Coordinates: (-3, -5, -26, -16) EQUB 7 \ Extra edges: Scaffolds: 0, 4 \ Coordinates: 26, 16 EQUB 1 + 8 \ Object 4, Part 4: Scaffolds: (-5, -1, -0, 0) \ Coordinates: (-5, -17, -26, 26) EQUB 5 + 8 \ Object 4, Part 5: Scaffolds: (7, -5, -7, 7) \ Coordinates: (1, -5, -1, 1) EQUB 6 \ Object 4, Part 6: Scaffolds: (4, 6, -2, 2) \ Coordinates: (6, 3, -16, 16) EQUB 1 + 8 \ Object 5, Part 0: Scaffolds: (-2, -1, -0, 0) \ Coordinates: (-3, -17, -26, 26) EQUB 2 \ Object 6, Part 0: Scaffolds: (0, 2, -1, 1) \ Coordinates: (16, 1, -10, 10) EQUB 4 + 8 \ Object 7, Part 0: Scaffolds: (1, -4, -0, 0) \ Coordinates: (20, -8, -28, 28) EQUB 2 + 8 \ Object 7, Part 1: Scaffolds: (-4, -2, -1, -3) \ Coordinates: (-8, -18, -20, -16) EQUB 2 + 8 \ Object 7, Part 2: Scaffolds: (-4, -2, 3, 1) \ Coordinates: (-8, -18, 16, 20) EQUB 0 + 8 \ Object 8, Part 0: Scaffolds: (2, -0, -3, 3) \ Coordinates: (3, -18, -2, 2) EQUB 2 \ Object 8, Part 1: Scaffolds: (1, 2, -3, 1) \ Coordinates: (16, 3, -2, 16) EQUB 2 + 8 \ Object 9, Part 0: Scaffolds: (1, -2, -0, 0) \ Coordinates: (12, -10, -16, 16) EQUB 0 + 8 \ Object 9, Part 1: Scaffolds: (-2, -0, -3, 3) \ Coordinates: (-10, -16, -3, 3) EQUB 4 \ Object 10, Part 0: Scaffolds: (3, 4, -0, 0) \ Coordinates: (4, 1, -10, 10) EQUB 2 + 8 \ Object 10, Part 1: Scaffolds: (4, -2, -0, -1) \ Coordinates: (1, -6, -10, -9) EQUB 3 \ Object 10, Part 2: Scaffolds: (0, 3, 1, 0) \ Coordinates: (10, 4, 9, 10) EQUB 3 \ Object 11, Part 0: Scaffolds: (1, 3, -0, 1) \ Coordinates: (8, 5, -10, 8) EQUB 2 + 8 \ Object 11, Part 1: Scaffolds: (3, -2, -0, -1) \ Coordinates: (5, -6, -10, -8) EQUB 3 \ Object 12, Part 0: Scaffolds: (1, 3, -1, 0) \ Coordinates: (8, 5, -8, 10) EQUB 2 + 8 \ Object 12, Part 1: Scaffolds: (3, -2, 1, 0) \ Coordinates: (5, -6, 8, 10) \ ****************************************************************************** \ \ Name: rightTyrePixels \ Type: Variable \ Category: Dashboard \ Summary: Pixels along the edge of the right tyre \ Deep dive: Drawing around the dashboard \ \ ------------------------------------------------------------------------------ \ \ Contains a pixel byte for the white border (colour 2) along the edge of the \ right tyre. \ \ The tyreEdgeIndex table maps track line numbers to entries in this table. \ \ Each pixel is a colour 2 pixel, so the high nibble contains a 1 and the low \ nibble contains a 0, to give colour %10. Colour 2 is mapped to white at this \ point of the custom screen. \ \ ****************************************************************************** .rightTyrePixels EQUB %00000000 EQUB %00010000 EQUB %00110000 EQUB %00100000 EQUB %01100000 EQUB %01110000 EQUB %01000000 \ ****************************************************************************** \ \ Name: token0 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 0 \ Deep dive: Text tokens \ \ ****************************************************************************** .token0 EQUS "FORMULA 3 " \ Print "FORMULA 3 " EQUS "CHAMPIONSHIP" \ Print "CHAMPIONSHIP" EQUB 255 \ End token EQUB &81, &81 \ These bytes appear to be unused EQUB &81, &81 \ ****************************************************************************** \ \ Name: dashData12 \ Type: Variable \ Category: Screen buffer \ Summary: Contains code that gets moved into screen memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData12 SKIP 52 \ Populated with code from &7CCF to &7D02 \ ****************************************************************************** \ \ Name: objectLeft \ Type: Variable \ Category: 3D objects \ Summary: Scaffold measurements for the left of each object part \ Deep dive: Object definitions \ Scaling objects with scaffolds \ \ ------------------------------------------------------------------------------ \ \ Entries contain indexes into the scaledScaffold table. n + 8 points to the \ negative value of n (as scaledScaffold+8 is filled with the negative of \ scaledScaffold). \ \ ****************************************************************************** .objectLeft EQUB 1 + 8 \ Object 0, Part 0: Scaffolds: (-7, -6, -1, -2) \ Coordinates: (-4, -5, -22, -18) EQUB 2 \ Object 0, Part 1: Scaffolds: (-7, -6, 2, 1) \ Coordinates: (-4, -5, 18, 22) EQUB 0 + 8 \ Object 0, Part 2: Scaffolds: (-6, -3, -0, -4) \ Coordinates: (-5, -17, -24, -16) EQUB 4 \ Object 0, Part 3: Scaffolds: (-6, -3, 4, 0) \ Coordinates: (-5, -17, 16, 24) EQUB 2 + 8 \ Object 0, Part 4: Scaffolds: (-5, -3, -2, 2) \ Coordinates: (-8, -17, -18, 18) EQUB 5 + 8 \ Object 1, Part 0: Scaffolds: (5, 6, -5, 5) \ Coordinates: (5, 2, -5, 5) EQUB 3 + 8 \ Object 1, Part 1: Scaffolds: (6, -7, -3, 3) \ Coordinates: (2, -1, -8, 8) EQUB 1 + 8 \ Object 1, Part 2: Scaffolds: (-7, -5, -1, 1) \ Coordinates: (-1, -5, -12, 12) EQUB 6 + 8 \ Object 1, Part 3: Scaffolds: (4, 5, -6, 6) \ Coordinates: (6, 5, -2, 2) EQUB 0 + 8 \ Object 2, Part 0: Scaffolds: (-6, -5, -0, -4) \ Coordinates: (-3, -5, -26, -16) EQUB 4 \ Extra edges: Scaffolds: 0, 4 \ Coordinates: 26, 16 EQUB 0 + 8 \ Object 2, Part 2: Scaffolds: (-5, -3, -0, 0) \ Coordinates: (-5, -17, -26, 26) EQUB 1 + 8 \ Object 2, Part 3: Scaffolds: (-7, -6, -1, -2) \ Coordinates: (-2, -3, -24, -18) EQUB 2 \ Object 2, Part 4: Scaffolds: (-7, -6, 2, 1) \ Coordinates: (-2, -3, 18, 24) EQUB 0 + 8 \ Object 3, Part 0: Scaffolds: (2, 3, -0, 0) \ Coordinates: (6, 4, -16, 16) EQUB 0 + 8 \ Object 3, Part 1: Scaffolds: (3, 4, -0, 0) \ Coordinates: (4, 3, -16, 16) EQUB 5 + 8 \ Object 3, Part 2: Scaffolds: (4, -1, -5, 5) \ Coordinates: (3, -10, -1, 1) EQUB 5 + 8 \ Object 3, Part 3: Scaffolds: (-1, -0, -5, 5) \ Coordinates: (-10, -16, -1, 1) EQUB 4 + 8 \ Object 4, Part 0: Scaffolds: (6, -7, -4, 4) \ Coordinates: (3, -1, -6, 6) EQUB 3 + 8 \ Object 4, Part 1: Scaffolds: (-7, -5, -3, 3) \ Coordinates: (-1, -5, -12, 12) EQUB 0 + 8 \ Object 4, Part 2: Scaffolds: (-6, -5, -0, -2) \ Coordinates: (-3, -5, -26, -16) EQUB 2 \ Extra edges: Scaffolds: 0, 4 \ Coordinates: 26, 16 EQUB 0 + 8 \ Object 4, Part 4: Scaffolds: (-5, -1, -0, 0) \ Coordinates: (-5, -17, -26, 26) EQUB 7 + 8 \ Object 4, Part 5: Scaffolds: (7, -5, -7, 7) \ Coordinates: (1, -5, -1, 1) EQUB 2 + 8 \ Object 4, Part 6: Scaffolds: (4, 6, -2, 2) \ Coordinates: (6, 3, -16, 16) EQUB 0 + 8 \ Object 5, Part 0: Scaffolds: (-2, -1, -0, 0) \ Coordinates: (-3, -17, -26, 26) EQUB 1 + 8 \ Object 6, Part 0: Scaffolds: (0, 2, -1, 1) \ Coordinates: (16, 1, -10, 10) EQUB 0 + 8 \ Object 7, Part 0: Scaffolds: (1, -4, -0, 0) \ Coordinates: (20, -8, -28, 28) EQUB 1 + 8 \ Object 7, Part 1: Scaffolds: (-4, -2, -1, -3) \ Coordinates: (-8, -18, -20, -16) EQUB 3 \ Object 7, Part 2: Scaffolds: (-4, -2, 3, 1) \ Coordinates: (-8, -18, 16, 20) EQUB 3 + 8 \ Object 8, Part 0: Scaffolds: (2, -0, -3, 3) \ Coordinates: (3, -18, -2, 2) EQUB 3 + 8 \ Object 8, Part 1: Scaffolds: (1, 2, -3, 1) \ Coordinates: (16, 3, -2, 16) EQUB 0 + 8 \ Object 9, Part 0: Scaffolds: (1, -2, -0, 0) \ Coordinates: (12, -10, -16, 16) EQUB 3 + 8 \ Object 9, Part 1: Scaffolds: (-2, -0, -3, 3) \ Coordinates: (-10, -16, -3, 3) EQUB 0 + 8 \ Object 10, Part 0: Scaffolds: (3, 4, -0, 0) \ Coordinates: (4, 1, -10, 10) EQUB 0 + 8 \ Object 10, Part 1: Scaffolds: (4, -2, -0, -1) \ Coordinates: (1, -6, -10, -9) EQUB 1 \ Object 10, Part 2: Scaffolds: (0, 3, 1, 0) \ Coordinates: (10, 4, 9, 10) EQUB 0 + 8 \ Object 11, Part 0: Scaffolds: (1, 3, -0, 1) \ Coordinates: (8, 5, -10, 8) EQUB 0 + 8 \ Object 11, Part 1: Scaffolds: (3, -2, -0, -1) \ Coordinates: (5, -6, -10, -8) EQUB 1 + 8 \ Object 12, Part 0: Scaffolds: (1, 3, -1, 0) \ Coordinates: (8, 5, -8, 10) EQUB 1 \ Object 12, Part 1: Scaffolds: (3, -2, 1, 0) \ Coordinates: (5, -6, 8, 10) \ ****************************************************************************** \ \ Name: leftTyreMask \ Type: Variable \ Category: Dashboard \ Summary: Pixel mask for the edge of the left tyre \ Deep dive: Drawing around the dashboard \ \ ------------------------------------------------------------------------------ \ \ Contains a mask byte for the track pixels along the edge of the left tyre. \ \ The tyreEdgeIndex table maps track line numbers to entries in this table. \ \ Set bits correspond to the track pixels, while clear bits correspond to the \ tyre pixels. \ \ ****************************************************************************** .leftTyreMask EQUB %11111111 EQUB %01110111 EQUB %00110011 EQUB %00110011 EQUB %00010001 EQUB %00010001 EQUB %00010001 \ ****************************************************************************** \ \ Name: token42 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 42 \ Deep dive: Text tokens \ \ ****************************************************************************** .token42 EQUB 160 + 11 \ Print 11 spaces EQUS "YOUR TIME " \ Print "YOUR TIME " EQUS "IS UP!" \ Print "IS UP!" EQUB 160 + 11 \ Print 11 spaces EQUB 255 \ End token \ ****************************************************************************** \ \ Name: token17 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 17 \ Deep dive: Text tokens \ \ ****************************************************************************** .token17 EQUS "PRESS " \ Print "PRESS " EQUB 255 \ End token EQUB &81, &81 \ These bytes appear to be unused EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81 \ ****************************************************************************** \ \ Name: dashData13 \ Type: Variable \ Category: Screen buffer \ Summary: Contains code that gets moved into screen memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData13 SKIP 41 \ Populated with code from &7CA6 to &7CCE \ ****************************************************************************** \ \ Name: objectRight \ Type: Variable \ Category: 3D objects \ Summary: Scaffold measurements for the right of each object part \ Deep dive: Object definitions \ Scaling objects with scaffolds \ \ ------------------------------------------------------------------------------ \ \ Entries contain indexes into the scaledScaffold table. n + 8 points to the \ negative value of n (as scaledScaffold+8 is filled with the negative of \ scaledScaffold). \ \ ****************************************************************************** .objectRight EQUB 2 + 8 \ Object 0, Part 0: Scaffolds: (-7, -6, -1, -2) \ Coordinates: (-4, -5, -22, -18) EQUB 1 \ Object 0, Part 1: Scaffolds: (-7, -6, 2, 1) \ Coordinates: (-4, -5, 18, 22) EQUB 4 + 8 \ Object 0, Part 2: Scaffolds: (-6, -3, -0, -4) \ Coordinates: (-5, -17, -24, -16) EQUB 0 \ Object 0, Part 3: Scaffolds: (-6, -3, 4, 0) \ Coordinates: (-5, -17, 16, 24) EQUB 2 \ Object 0, Part 4: Scaffolds: (-5, -3, -2, 2) \ Coordinates: (-8, -17, -18, 18) EQUB 5 \ Object 1, Part 0: Scaffolds: (5, 6, -5, 5) \ Coordinates: (5, 2, -5, 5) EQUB 3 \ Object 1, Part 1: Scaffolds: (6, -7, -3, 3) \ Coordinates: (2, -1, -8, 8) EQUB 1 \ Object 1, Part 2: Scaffolds: (-7, -5, -1, 1) \ Coordinates: (-1, -5, -12, 12) EQUB 6 \ Object 1, Part 3: Scaffolds: (4, 5, -6, 6) \ Coordinates: (6, 5, -2, 2) EQUB 4 + 8 \ Object 2, Part 0: Scaffolds: (-6, -5, -0, -4) \ Coordinates: (-3, -5, -26, -16) EQUB 1 + 8 \ Extra edges: Scaffolds: 0, 4 \ Coordinates: 26, 16 EQUB 0 \ Object 2, Part 2: Scaffolds: (-5, -3, -0, 0) \ Coordinates: (-5, -17, -26, 26) EQUB 2 + 8 \ Object 2, Part 3: Scaffolds: (-7, -6, -1, -2) \ Coordinates: (-2, -3, -24, -18) EQUB 1 \ Object 2, Part 4: Scaffolds: (-7, -6, 2, 1) \ Coordinates: (-2, -3, 18, 24) EQUB 0 \ Object 3, Part 0: Scaffolds: (2, 3, -0, 0) \ Coordinates: (6, 4, -16, 16) EQUB 0 \ Object 3, Part 1: Scaffolds: (3, 4, -0, 0) \ Coordinates: (4, 3, -16, 16) EQUB 5 \ Object 3, Part 2: Scaffolds: (4, -1, -5, 5) \ Coordinates: (3, -10, -1, 1) EQUB 5 \ Object 3, Part 3: Scaffolds: (-1, -0, -5, 5) \ Coordinates: (-10, -16, -1, 1) EQUB 4 \ Object 4, Part 0: Scaffolds: (6, -7, -4, 4) \ Coordinates: (3, -1, -6, 6) EQUB 3 \ Object 4, Part 1: Scaffolds: (-7, -5, -3, 3) \ Coordinates: (-1, -5, -12, 12) EQUB 2 + 8 \ Object 4, Part 2: Scaffolds: (-6, -5, -0, -2) \ Coordinates: (-3, -5, -26, -16) EQUB 1 + 8 \ Extra edges: Scaffolds: 0, 4 \ Coordinates: 26, 16 EQUB 0 \ Object 4, Part 4: Scaffolds: (-5, -1, -0, 0) \ Coordinates: (-5, -17, -26, 26) EQUB 7 \ Object 4, Part 5: Scaffolds: (7, -5, -7, 7) \ Coordinates: (1, -5, -1, 1) EQUB 2 \ Object 4, Part 6: Scaffolds: (4, 6, -2, 2) \ Coordinates: (6, 3, -16, 16) EQUB 0 \ Object 5, Part 0: Scaffolds: (-2, -1, -0, 0) \ Coordinates: (-3, -17, -26, 26) EQUB 1 \ Object 6, Part 0: Scaffolds: (0, 2, -1, 1) \ Coordinates: (16, 1, -10, 10) EQUB 0 \ Object 7, Part 0: Scaffolds: (1, -4, -0, 0) \ Coordinates: (20, -8, -28, 28) EQUB 3 + 8 \ Object 7, Part 1: Scaffolds: (-4, -2, -1, -3) \ Coordinates: (-8, -18, -20, -16) EQUB 1 \ Object 7, Part 2: Scaffolds: (-4, -2, 3, 1) \ Coordinates: (-8, -18, 16, 20) EQUB 3 \ Object 8, Part 0: Scaffolds: (2, -0, -3, 3) \ Coordinates: (3, -18, -2, 2) EQUB 1 \ Object 8, Part 1: Scaffolds: (1, 2, -3, 1) \ Coordinates: (16, 3, -2, 16) EQUB 0 \ Object 9, Part 0: Scaffolds: (1, -2, -0, 0) \ Coordinates: (12, -10, -16, 16) EQUB 3 \ Object 9, Part 1: Scaffolds: (-2, -0, -3, 3) \ Coordinates: (-10, -16, -3, 3) EQUB 0 \ Object 10, Part 0: Scaffolds: (3, 4, -0, 0) \ Coordinates: (4, 1, -10, 10) EQUB 1 + 8 \ Object 10, Part 1: Scaffolds: (4, -2, -0, -1) \ Coordinates: (1, -6, -10, -9) EQUB 0 \ Object 10, Part 2: Scaffolds: (0, 3, 1, 0) \ Coordinates: (10, 4, 9, 10) EQUB 1 \ Object 11, Part 0: Scaffolds: (1, 3, -0, 1) \ Coordinates: (8, 5, -10, 8) EQUB 1 + 8 \ Object 11, Part 1: Scaffolds: (3, -2, -0, -1) \ Coordinates: (5, -6, -10, -8) EQUB 0 \ Object 12, Part 0: Scaffolds: (1, 3, -1, 0) \ Coordinates: (8, 5, -8, 10) EQUB 0 \ Object 12, Part 1: Scaffolds: (3, -2, 1, 0) \ Coordinates: (5, -6, 8, 10) \ ****************************************************************************** \ \ Name: rightTyreMask \ Type: Variable \ Category: Dashboard \ Summary: Pixel mask for the edge of the right tyre \ Deep dive: Drawing around the dashboard \ \ ------------------------------------------------------------------------------ \ \ Contains a mask byte for the track pixels along the edge of the right tyre. \ \ The tyreEdgeIndex table maps track line numbers to entries in this table. \ \ Set bits correspond to the track pixels, while clear bits correspond to the \ tyre pixels. \ \ ****************************************************************************** .rightTyreMask EQUB %11111111 EQUB %11101110 EQUB %11001100 EQUB %11001100 EQUB %10001000 EQUB %10001000 EQUB %10001000 \ ****************************************************************************** \ \ Name: token23 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 23 \ Deep dive: Text tokens \ \ ****************************************************************************** .token23 EQUB 200 + 54 \ Print token 54 ("FORMULA 3 CHAMPIONSHIP" header) EQUB 200 + 35 \ Print token 35 (cyan, move cursor to prompt position) EQUB 160 + 7 \ Print 7 spaces EQUB 200 + 11 \ Print token 11 ("ENTER ") EQUS "NAME OF" \ Print "NAME OF" EQUB 200 + 12 \ Print token 12 (" DRIVER") EQUB 31, 12, 17 \ Move text cursor to column 12, row 17 EQUB 131 \ Set foreground colour to yellow alphanumeric EQUS "____________" \ Print "____________" EQUB 31, 9, 16 \ Move text cursor to column 9, row 16 EQUB 133 \ Set foreground colour to magenta alphanumeric EQUB 200 + 16 \ Print token 16 (" > ") EQUB 255 \ End token \ ****************************************************************************** \ \ Name: token35 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 35 \ Deep dive: Text tokens \ \ ****************************************************************************** .token35 EQUB 31, 2, 10 \ Move text cursor to column 2, row 10 EQUB 134 \ Set foreground colour to cyan alphanumeric EQUB 255 \ End token EQUB &81, &81 \ These bytes appear to be unused EQUB &81, &81 EQUB &81 \ ****************************************************************************** \ \ Name: dashData14 \ Type: Variable \ Category: Screen buffer \ Summary: Contains code that gets moved into screen memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData14 SKIP 36 \ Populated with code from &7C82 to &7CA5 \ ****************************************************************************** \ \ Name: objectColour \ Type: Variable \ Category: 3D objects \ Summary: Data for the colour of each object part \ Deep dive: Object definitions \ \ ------------------------------------------------------------------------------ \ \ Entries contain colour numbers and flags. n + 16 draws an outside edge in the \ fill colour rather than the edge colour. n + 64 indicates that this is the \ last part in this object. n + 128 indicates that this is a four-edge object \ part. \ \ ****************************************************************************** .objectColour EQUB 10 \ Object 0, Part 0: Edge: 2 (white), Fill: 2 (white) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 10 \ Object 0, Part 1: Edge: 2 (white), Fill: 2 (white) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 8 \ Object 0, Part 2: Edge: 0 (black), Fill: 2 (white) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 8 \ Object 0, Part 3: Edge: 0 (black), Fill: 2 (white) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 5 + 64 \ Object 0, Part 4: Edge: 1 (red), Fill: 1 (red) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 8 \ Object 1, Part 0: Edge: 0 (black), Fill: 2 (white) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 8 \ Object 1, Part 1: Edge: 0 (black), Fill: 2 (white) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 9 \ Object 1, Part 2: Edge: 1 (red), Fill: 2 (white) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 10 + 64 \ Object 1, Part 3: Edge: 2 (white), Fill: 2 (white) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 8 + 128 \ Object 2, Part 0: Edge: 0 (black), Fill: 2 (white) \ Outside: 0 (no), Four-edge: 1 (yes) EQUB 8 \ Extra edges: Fill: 1 (red) EQUB 8 \ Object 2, Part 2: Edge: 0 (black), Fill: 2 (white) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 10 \ Object 2, Part 3: Edge: 2 (white), Fill: 2 (white) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 10 + 64 \ Object 2, Part 4: Edge: 2 (white), Fill: 2 (white) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 2 \ Object 3, Part 0: Edge: 2 (white), Fill: 0 (black) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 0 \ Object 3, Part 1: Edge: 0 (black), Fill: 0 (black) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 2 \ Object 3, Part 2: Edge: 2 (white), Fill: 0 (black) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 10 + 64 \ Object 3, Part 3: Edge: 2 (white), Fill: 2 (white) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 8 \ Object 4, Part 0: Edge: 0 (black), Fill: 2 (white) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 5 \ Object 4, Part 1: Edge: 1 (red), Fill: 1 (red) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 8 + 128 \ Object 4, Part 2: Edge: 0 (black), Fill: 2 (white) \ Outside: 0 (no), Four-edge: 1 (yes) EQUB 8 \ Extra edges: Fill: 1 (red) EQUB 8 \ Object 4, Part 4: Edge: 0 (black), Fill: 2 (white) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 2 \ Object 4, Part 5: Edge: 2 (white), Fill: 0 (black) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 2 + 64 \ Object 4, Part 6: Edge: 2 (white), Fill: 0 (black) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 8 + 64 \ Object 5, Part 0: Edge: 0 (black), Fill: 2 (white) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 2 + 16 + 64 \ Object 6, Part 0: Edge: 2 (white), Fill: 0 (black) \ Outside: 1 (yes), Four-edge: 0 (no) EQUB 8 \ Object 7, Part 0: Edge: 0 (black), Fill: 2 (white) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 1 + 16 \ Object 7, Part 1: Edge: 1 (red), Fill: 0 (black) \ Outside: 1 (yes), Four-edge: 0 (no) EQUB 1 + 16 + 64 \ Object 7, Part 2: Edge: 1 (red), Fill: 0 (black) \ Outside: 1 (yes), Four-edge: 0 (no) EQUB 0 \ Object 8, Part 0: Edge: 0 (black), Fill: 0 (black) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 8 + 64 \ Object 8, Part 1: Edge: 0 (black), Fill: 2 (white) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 10 \ Object 9, Part 0: Edge: 2 (white), Fill: 2 (white) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 2 + 16 + 64 \ Object 9, Part 1: Edge: 2 (white), Fill: 0 (black) \ Outside: 1 (yes), Four-edge: 0 (no) EQUB 0 \ Object 10, Part 0: Edge: 0 (black), Fill: 0 (black) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 0 \ Object 10, Part 1: Edge: 0 (black), Fill: 0 (black) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 0 + 64 \ Object 10, Part 2: Edge: 0 (black), Fill: 0 (black) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 0 \ Object 11, Part 0: Edge: 0 (black), Fill: 0 (black) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 0 + 64 \ Object 11, Part 1: Edge: 0 (black), Fill: 0 (black) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 0 \ Object 12, Part 0: Edge: 0 (black), Fill: 0 (black) \ Outside: 0 (no), Four-edge: 0 (no) EQUB 0 + 64 \ Object 12, Part 1: Edge: 0 (black), Fill: 0 (black) \ Outside: 0 (no), Four-edge: 0 (no) \ ****************************************************************************** \ \ Name: gearNumberText \ Type: Variable \ Category: Text \ Summary: The character to print on the gear stick for each gear \ \ ****************************************************************************** .gearNumberText EQUS "RN12345" \ ****************************************************************************** \ \ Name: token43 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 43 \ Deep dive: Text tokens \ \ ****************************************************************************** .token43 EQUS "Position" \ Print "Position" EQUB 160 + 8 \ Print 8 spaces EQUS "In front:" \ Print "In front:" EQUB 160 + 13 \ Print 13 spaces EQUB 255 \ End token \ ****************************************************************************** \ \ Name: token44 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 44 \ Deep dive: Text tokens \ \ ****************************************************************************** .token44 EQUS "Laps to go" \ Print "Laps to go" EQUB 160 + 8 \ Print 8 spaces EQUS "Behind:" \ Print "Behind:" EQUB 160 + 18 \ Print 18 spaces EQUB 255 \ End token \ ****************************************************************************** \ \ Name: token45 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 45 \ Deep dive: Text tokens \ \ ****************************************************************************** .token45 EQUB 160 + 38 \ Print 38 spaces EQUB 255 \ End token EQUB &81, &81 \ These bytes appear to be unused \ ****************************************************************************** \ \ Name: dashData15 \ Type: Variable \ Category: Screen buffer \ Summary: Contains code that gets moved into screen memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData15 SKIP 36 \ Populated with code from &7C5E to &7C81 \ ****************************************************************************** \ \ Name: Print2DigitBCD \ Type: Subroutine \ Category: Text \ Summary: Print a binary coded decimal (BCD) number in the specified format \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ A The number to print (in BCD) \ \ G Flags to control how the number is printed: \ \ * Bit 7: clear = do not print leading zeroes \ set = print leading zeroes \ \ * Bit 6: clear = print second digit \ set = do not print second digit \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ G G is shifted left by two places, so bits 4 and 5 will be \ used to determine the printing style in the next call to \ Print2DigitBCD \ \ ------------------------------------------------------------------------------ \ \ Other entry points: \ \ Print2DigitBCD-6 Print the number at screen coordinate (X, Y), where X \ is the character column and Y is the pixel row of the \ bottom of the character \ \ ****************************************************************************** STX xCursor \ Set the cursor to (X, Y), so we print the number at STY yCursor \ the specified screen location .Print2DigitBCD PHA \ Store A on the stack so we can retrieve it later LSR A \ Shift the high nibble of A into bits 0-3, so A LSR A \ contains the first digit of the BCD number LSR A LSR A BNE pnum1 \ If the result is non-zero, jump to pnum1 to print the \ digit in A \ Otherwise the first digit is a zero, which we either \ print as a capital "O" (so it doesn't have a line \ through it), or as a space, depending on the setting \ in G, which controls whether or not to print leading \ zeroes LDA #'O'-'0' \ Set A so we print a capital "O" in pnum1 BIT G \ If bit 7 of G is set, jump to pnum1 to print a capital BMI pnum1 \ "O" LDA #LO(' '-'0') \ Otherwise bit 7 of G is clear and we do not print \ leading zeroes, so instead set A so we print a space \ in pnum1 .pnum1 CLC \ Print the high nibble in A as a digit (or, if the high ADC #'0' \ nibble is zero, print a capital "O" or a space, as per JSR PrintCharacter \ the above) \ Now for the second digit ASL G \ Shift G to the left, so bit 6 is now in bit 7 PLA \ Retrieve the original value of A, which contains the \ BCD number to print ASL G \ If bit 7 of G is set (i.e. bit 6 of the original G), BCS pnum3 \ jump to pnum3 to skip printing the second digit, and \ return from the subroutine AND #%00001111 \ Extract the low nibble of the BCD number into A BNE pnum2 \ If the low nibble is non-zero, jump to pnum2 to skip \ the following instruction LDA #'O'-'0' \ Set A so we print a capital "O" in pnum2 .pnum2 CLC \ Print the low nibble in A as a digit (or, if the low ADC #'0' \ nibble is zero, print a capital "O") JSR PrintCharacter .pnum3 RTS \ Return from the subroutine EQUB &FF \ This byte appears to be unused \ ****************************************************************************** \ \ Name: token22 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 22 \ Deep dive: Text tokens \ \ ****************************************************************************** .token22 EQUB 200 + 54 \ Print token 54 ("FORMULA 3 CHAMPIONSHIP" header) EQUB 200 + 36 \ Print token 36 (menu option 1 with "PRESS" prompt) EQUB 200 + 18 \ Print token 18 (" 5") EQUB 200 + 13 \ Print token 13 (" mins") EQUB 200 + 37 \ Print token 37 (menu option 2) EQUB 200 + 19 \ Print token 19 ("10") EQUB 200 + 13 \ Print token 13 (" mins") EQUB 200 + 38 \ Print token 38 (menu option 3) EQUB 200 + 20 \ Print token 20 ("20") EQUB 200 + 13 \ Print token 13 (" mins") EQUB 200 + 35 \ Print token 35 (cyan, move cursor to prompt position) EQUB 200 + 10 \ Print token 10 ("SELECT ") EQUS "DURATION OF " \ Print "DURATION OF " EQUS "QUALIFYING LAPS" \ Print "QUALIFYING LAPS" EQUB 255 \ End token \ ****************************************************************************** \ \ Name: token16 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 16 \ Deep dive: Text tokens \ \ ****************************************************************************** .token16 EQUS " ] " \ Print " ] " EQUB 255 \ End token \ ****************************************************************************** \ \ Name: dashData16 \ Type: Variable \ Category: Screen buffer \ Summary: Contains code that gets moved into screen memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData16 SKIP 36 \ Populated with code from &7C3A to &7C5D \ ****************************************************************************** \ \ Name: carSpeedLo \ Type: Variable \ Category: Car geometry \ Summary: Low byte of each car's forward speed \ \ ------------------------------------------------------------------------------ \ \ Stored as an 8-bit value (carSpeedHi carSpeedLo). \ \ ****************************************************************************** .carSpeedLo IF _ACORNSOFT OR _4TRACKS EQUB &FF, &88 \ These values are workspace noise and have no meaning EQUB &88, &CC EQUB &CC, &CC EQUB &CC, &CC EQUB &CC, &EE EQUB &EE, &EE EQUB &EE, &FF EQUB &FF, &FF EQUB &88, &88 EQUB &88, &CC ELIF _SUPERIOR OR _REVSPLUS SKIP 20 ENDIF \ ****************************************************************************** \ \ Name: totalPointsLo \ Type: Variable \ Category: Drivers \ Summary: Low byte of total accumulated points for each driver \ \ ------------------------------------------------------------------------------ \ \ Indexed by driver number (0 to 19). \ \ Gets set in InitialiseDrivers. \ \ Stored as a 24-bit value (totalPointsTop totalPointsHi totalPointsLo). \ \ ****************************************************************************** .totalPointsLo IF _ACORNSOFT OR _4TRACKS EQUB &CC, &CC \ These values are workspace noise and have no meaning EQUB &EE, &FF EQUB &FF, &88 EQUB &CC, &EE EQUB &FF, &FF EQUB &FF, &FF EQUB &FF, &FF EQUB &FF, &FF EQUB &FF, &FF EQUB &FF, &FF ELIF _SUPERIOR OR _REVSPLUS SKIP 20 ENDIF \ ****************************************************************************** \ \ Name: racePointsLo \ Type: Variable \ Category: Drivers \ Summary: Used to store the low byte of the race points being awarded to \ the driver in race position X \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (racePointsHi racePointsLo). \ \ ****************************************************************************** .racePointsLo IF _ACORNSOFT OR _4TRACKS EQUB &FF, &FF \ These values are workspace noise and have no meaning EQUB &FF, &FF EQUB &FF, &FF EQUB &FF, &FF ELIF _SUPERIOR OR _REVSPLUS SKIP 8 ENDIF \ ****************************************************************************** \ \ Name: token39 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 39 \ Deep dive: Text tokens \ \ ****************************************************************************** .token39 EQUB 200 + 36 \ Print token 36 (menu option 1 with "PRESS" prompt) EQUS "PRACTICE" \ Print "PRACTICE" EQUB 200 + 37 \ Print token 37 (menu option 2) EQUS "COMPETITION" \ Print "COMPETITION" EQUB 255 \ End token EQUB &FF \ This byte appears to be unused \ ****************************************************************************** \ \ Name: token48 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 48 \ Deep dive: Text tokens \ \ ****************************************************************************** .token48 EQUB 160 + 13 \ Print 13 spaces EQUS "PLEASE" \ Print "PLEASE" EQUB 160 + 2 \ Print 2 spaces EQUS "WAIT" \ Print "WAIT" EQUB 160 + 13 \ Print 13 spaces EQUB 255 \ End token \ ****************************************************************************** \ \ Name: token49 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 49 \ Deep dive: Text tokens \ \ ****************************************************************************** .token49 EQUB 31, 9, 2 \ Move text cursor to column 9, row 2 EQUB 255 \ End token EQUB &81, &81 \ These bytes appear to be unused EQUB &81 \ ****************************************************************************** \ \ Name: dashData17 \ Type: Variable \ Category: Screen buffer \ Summary: Contains code that gets moved into screen memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData17 SKIP 36 \ Populated with code from &7C16 to &7C39 \ ****************************************************************************** \ \ Name: leftDashMask \ Type: Variable \ Category: Dashboard \ Summary: Pixel mask for the left edge of the dashboard \ Deep dive: Drawing around the dashboard \ \ ------------------------------------------------------------------------------ \ \ Contains a mask byte for the track pixels along the left edge of the central \ part of the dashboard. \ \ There is a byte for each track line from 43 (the track line at the top of the \ dashboard) down to 3 (the lowest track line, just above where the wing mirror \ joins the car body). Lines 0 to 2 are not used. \ \ Set bits correspond to the track pixels, while clear bits correspond to the \ dashboard pixels. \ \ ****************************************************************************** .leftDashMask EQUB %11111111 \ Line 0 EQUB %11111111 \ Line 1 EQUB %10001000 \ Line 2 EQUB %10001000 \ Line 3 EQUB %11001100 \ Line 4 EQUB %11101110 \ Line 5 EQUB %11111111 \ Line 6 EQUB %10001000 \ Line 7 EQUB %10001000 \ Line 8 EQUB %11001100 \ Line 9 EQUB %11101110 \ Line 10 EQUB %11111111 \ Line 11 EQUB %10001000 \ Line 12 EQUB %11001100 \ Line 13 EQUB %11101110 \ Line 14 EQUB %11111111 \ Line 15 EQUB %10001000 \ Line 16 EQUB %11001100 \ Line 17 EQUB %11101110 \ Line 18 EQUB %11111111 \ Line 19 EQUB %10001000 \ Line 20 EQUB %11001100 \ Line 21 EQUB %11101110 \ Line 22 EQUB %11111111 \ Line 23 EQUB %10001000 \ Line 24 EQUB %11001100 \ Line 25 EQUB %11101110 \ Line 26 EQUB %11111111 \ Line 27 EQUB %10001000 \ Line 28 EQUB %10001000 \ Line 29 EQUB %11001100 \ Line 30 EQUB %11001100 \ Line 31 EQUB %11001100 \ Line 32 EQUB %11101110 \ Line 33 EQUB %11101110 \ Line 34 EQUB %11101110 \ Line 35 EQUB %11111111 \ Line 36 EQUB %11111111 \ Line 37 EQUB %11111111 \ Line 38 EQUB %10001000 \ Line 39 EQUB %10001000 \ Line 40 EQUB %10001000 \ Line 41 EQUB %11001100 \ Line 42 EQUB %11001100 \ Line 43 \ ****************************************************************************** \ \ Name: colourPalette \ Type: Variable \ Category: Drawing pixels \ Summary: The main colour palette that maps logical colours 0 to 3 to \ physical colours \ \ ****************************************************************************** .colourPalette EQUB %00000000 \ Four pixels of colour 0 EQUB %00001111 \ Four pixels of colour 1 EQUB %11110000 \ Four pixels of colour 2 EQUB %11111111 \ Four pixels of colour 3 \ ****************************************************************************** \ \ Name: dashDataOffset \ Type: Variable \ Category: Screen buffer \ Summary: Offset of the dash data within each dash data block \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashDataOffset EQUB dashData0 - (dashData + &80 * 0) - 1 EQUB dashData1 - (dashData + &80 * 1) - 1 EQUB dashData2 - (dashData + &80 * 2) - 1 EQUB dashData3 - (dashData + &80 * 3) - 1 EQUB dashData4 - (dashData + &80 * 4) - 1 EQUB dashData5 - (dashData + &80 * 5) - 1 EQUB dashData6 - (dashData + &80 * 6) - 1 EQUB dashData7 - (dashData + &80 * 7) - 1 EQUB dashData8 - (dashData + &80 * 8) - 1 EQUB dashData9 - (dashData + &80 * 9) - 1 EQUB dashData10 - (dashData + &80 * 10) - 1 EQUB dashData11 - (dashData + &80 * 11) - 1 EQUB dashData12 - (dashData + &80 * 12) - 1 EQUB dashData13 - (dashData + &80 * 13) - 1 EQUB dashData14 - (dashData + &80 * 14) - 1 EQUB dashData15 - (dashData + &80 * 15) - 1 EQUB dashData16 - (dashData + &80 * 16) - 1 EQUB dashData17 - (dashData + &80 * 17) - 1 EQUB dashData18 - (dashData + &80 * 18) - 1 EQUB dashData19 - (dashData + &80 * 19) - 1 EQUB dashData20 - (dashData + &80 * 20) - 1 EQUB dashData21 - (dashData + &80 * 21) - 1 EQUB dashData22 - (dashData + &80 * 22) - 1 EQUB dashData23 - (dashData + &80 * 23) - 1 EQUB dashData24 - (dashData + &80 * 24) - 1 EQUB dashData25 - (dashData + &80 * 25) - 1 EQUB dashData26 - (dashData + &80 * 26) - 1 EQUB dashData27 - (dashData + &80 * 27) - 1 EQUB dashData28 - (dashData + &80 * 28) - 1 EQUB dashData29 - (dashData + &80 * 29) - 1 EQUB dashData30 - (dashData + &80 * 30) - 1 EQUB dashData31 - (dashData + &80 * 31) - 1 EQUB dashData32 - (dashData + &80 * 32) - 1 EQUB dashData33 - (dashData + &80 * 33) - 1 EQUB dashData34 - (dashData + &80 * 34) - 1 EQUB dashData35 - (dashData + &80 * 35) - 1 EQUB dashData36 - (dashData + &80 * 36) - 1 EQUB dashData37 - (dashData + &80 * 37) - 1 EQUB dashData38 - (dashData + &80 * 38) - 1 EQUB dashData39 - (dashData + &80 * 39) - 1 EQUB dashData40 - (dashData + &80 * 40) - 1 EQUB &FF, &81 \ These bytes appear to be unused EQUB &81 \ ****************************************************************************** \ \ Name: dashData18 \ Type: Variable \ Category: Screen buffer \ Summary: Contains code that gets moved into screen memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData18 SKIP 36 \ Populated with code from &7BF2 to &7C15 \ ****************************************************************************** \ \ Name: rightDashMask \ Type: Variable \ Category: Dashboard \ Summary: Pixel mask for the right edge of the dashboard \ Deep dive: Drawing around the dashboard \ \ ------------------------------------------------------------------------------ \ \ Contains a mask byte for the track pixels along the right edge of the central \ part of the dashboard. \ \ There is a byte for each track line from 43 (the track line at the top of the \ dashboard) down to 3 (the lowest track line, just above where the wing mirror \ joins the car body). Lines 0 to 2 are not used. \ \ Set bits correspond to the track pixels, while clear bits correspond to the \ dashboard pixels. \ \ ****************************************************************************** .rightDashMask EQUB %11111111 \ Line 0 EQUB %11111111 \ Line 1 EQUB %00010001 \ Line 2 EQUB %00010001 \ Line 3 EQUB %00110011 \ Line 4 EQUB %01110111 \ Line 5 EQUB %11111111 \ Line 6 EQUB %00010001 \ Line 7 EQUB %00010001 \ Line 8 EQUB %00110011 \ Line 9 EQUB %01110111 \ Line 10 EQUB %11111111 \ Line 11 EQUB %00010001 \ Line 12 EQUB %00110011 \ Line 13 EQUB %01110111 \ Line 14 EQUB %11111111 \ Line 15 EQUB %00010001 \ Line 16 EQUB %00110011 \ Line 17 EQUB %01110111 \ Line 18 EQUB %11111111 \ Line 19 EQUB %00010001 \ Line 20 EQUB %00110011 \ Line 21 EQUB %01110111 \ Line 22 EQUB %11111111 \ Line 23 EQUB %00010001 \ Line 24 EQUB %00110011 \ Line 25 EQUB %01110111 \ Line 26 EQUB %11111111 \ Line 27 EQUB %00010001 \ Line 28 EQUB %00010001 \ Line 29 EQUB %00110011 \ Line 30 EQUB %00110011 \ Line 31 EQUB %00110011 \ Line 32 EQUB %01110111 \ Line 33 EQUB %01110111 \ Line 34 EQUB %01110111 \ Line 35 EQUB %11111111 \ Line 36 EQUB %11111111 \ Line 37 EQUB %11111111 \ Line 38 EQUB %00010001 \ Line 39 EQUB %00010001 \ Line 40 EQUB %00010001 \ Line 41 EQUB %00110011 \ Line 42 EQUB %00110011 \ Line 43 \ ****************************************************************************** \ \ Name: startDialHi \ Type: Variable \ Category: Drawing pixels \ Summary: The high byte of the screen address of the start of the dial hand \ on the rev counter \ \ ****************************************************************************** .startDialHi EQUB &75 \ Quadrant 0 (12:00 to 3:00) = &7566 EQUB &75 \ Quadrant 1 (3:00 to 6:00) = &7567 EQUB &75 \ Quadrant 2 (6:00 to 9:00) = &755F EQUB &75 \ Quadrant 3 (9:00 to 12:00) = &755E \ ****************************************************************************** \ \ Name: wheelPixels \ Type: Variable \ Category: Dashboard \ Summary: The number of pixels in the longest axis for the steering wheel \ line at various points in a quadrant \ Deep dive: Trigonometry \ \ ------------------------------------------------------------------------------ \ \ This table contains values that are used to calculate the coordinates of the \ end of the line on the steering wheel. \ \ The contents of the table are very close to the following (the values from \ the following calculation are shown in the comments below - they are close, \ but not quite a perfect match, so I haven't got this exactly right): \ \ FOR I%, 0, 37 \ EQUB INT(0.5 + 53 * COS((PI/4) * I% / 42)) \ NEXT \ \ This gives the length of the adjacent side of a right-angled triangle, with a \ hypotenuse of length 53, and an angle ranging from 0 to PI/4 (i.e. one \ eighth of a circle), split up into 42 points per eighth of a circle (so the \ table's 38 points cover just less than an eighth of a circle). \ \ In other words, if we have a clock whose centre is at the origin, then this \ table contains the x-coordinate of the end of a clock hand of length 53 as it \ moves from 3 o'clock to half past 4. \ \ ****************************************************************************** .wheelPixels EQUB 53 \ INT(0.5 + 53.00) = 53 EQUB 53 \ INT(0.5 + 52.99) = 53 EQUB 53 \ INT(0.5 + 52.96) = 53 EQUB 53 \ INT(0.5 + 52.92) = 53 EQUB 53 \ INT(0.5 + 52.85) = 53 EQUB 53 \ INT(0.5 + 52.77) = 53 EQUB 53 \ INT(0.5 + 52.67) = 53 EQUB 53 \ INT(0.5 + 52.55) = 53 EQUB 52 \ INT(0.5 + 52.41) = 52 EQUB 52 \ INT(0.5 + 52.25) = 52 EQUB 52 \ INT(0.5 + 52.08) = 52 EQUB 52 \ INT(0.5 + 51.88) = 52 EQUB 52 \ INT(0.5 + 51.67) = 52 EQUB 52 \ INT(0.5 + 51.44) = 51 (doesn't match) EQUB 51 \ INT(0.5 + 51.19) = 51 EQUB 51 \ INT(0.5 + 50.93) = 51 EQUB 51 \ INT(0.5 + 50.65) = 51 EQUB 50 \ INT(0.5 + 50.34) = 50 EQUB 50 \ INT(0.5 + 50.03) = 50 EQUB 50 \ INT(0.5 + 49.69) = 50 EQUB 49 \ INT(0.5 + 49.34) = 49 EQUB 49 \ INT(0.5 + 48.97) = 49 EQUB 48 \ INT(0.5 + 48.58) = 49 (doesn't match) EQUB 48 \ INT(0.5 + 48.17) = 48 EQUB 47 \ INT(0.5 + 47.75) = 48 (doesn't match) EQUB 47 \ INT(0.5 + 47.31) = 47 EQUB 46 \ INT(0.5 + 46.86) = 47 (doesn't match) EQUB 46 \ INT(0.5 + 46.39) = 46 EQUB 45 \ INT(0.5 + 45.90) = 46 (doesn't match) EQUB 45 \ INT(0.5 + 45.40) = 45 EQUB 44 \ INT(0.5 + 44.88) = 45 (doesn't match) EQUB 44 \ INT(0.5 + 44.34) = 44 EQUB 43 \ INT(0.5 + 43.79) = 44 (doesn't match) EQUB 42 \ INT(0.5 + 43.22) = 43 (doesn't match) EQUB 41 \ INT(0.5 + 42.64) = 43 (doesn't match) EQUB 40 \ INT(0.5 + 42.05) = 42 (doesn't match) EQUB 39 \ INT(0.5 + 41.44) = 41 (doesn't match) EQUB 38 \ INT(0.5 + 40.81) = 41 (doesn't match) \ ****************************************************************************** \ \ Name: token32 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 32 \ Deep dive: Text tokens \ \ ****************************************************************************** .token32 EQUB 160 + 2 \ Print 2 spaces EQUB 156 \ Set background colour to black EQUB 8, 8 \ Backspace to the left by two characters EQUB 200 + 31 \ Print token 31 (two spaces and configurable colours) EQUB 255 \ End token \ ****************************************************************************** \ \ Name: dashData19 \ Type: Variable \ Category: Screen buffer \ Summary: Contains code that gets moved into screen memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData19 SKIP 36 \ Populated with code from &7BCE to &7BF1 \ ****************************************************************************** \ \ Name: pixelsToRight \ Type: Variable \ Category: Drawing pixels \ Summary: Pixel byte with all the pixels to the right of position X set \ \ ****************************************************************************** .pixelsToRight EQUB %01110111 EQUB %00110011 EQUB %00010001 EQUB %00000000 \ ****************************************************************************** \ \ Name: configKeys \ Type: Variable \ Category: Keyboard \ Summary: Details of the configuration settings that are set by the shifted \ configuration keys \ \ ------------------------------------------------------------------------------ \ \ The low nibble of each setting indicates which configuration byte should be \ updated when this key is pressed, as an offset from configStop, and the high \ nibble contains the value that it should be set to. \ \ The values in this table correspond with the keys defined in the shiftedKeys \ table. \ \ ****************************************************************************** .configKeys \ SHIFT + key Bits affected Config byte Value EQUB &80 \ Right arrow Set bit 7 configStop &80 EQUB &01 \ f1 Clear all bits configJoystick &00 EQUB &C1 \ f2 Set bits 6 & 7 configJoystick &C0 EQUB &81 \ f2 Set bit 7 configJoystick &80 EQUB &C2 \ f4 Set bits 6 & 7 configVolume &C0 EQUB &42 \ f5 Set bit 6 configVolume &40 EQUB &C0 \ f0 Set bits 6 & 7 configStop &C0 EQUB &83 \ COPY Set bit 7 configPause &80 EQUB &43 \ DELETE Set bit 6 configPause &40 EQUB &20 \ f7 Set bit 5 configStop &20 IF _ACORNSOFT OR _4TRACKS EQUB &77, &BB \ These values are workspace noise and have no meaning ELIF _SUPERIOR OR _REVSPLUS EQUB &04 \ f3 Clear all bits configAssist &00 EQUB &84 \ f6 Set bit 7 configAssist &80 ENDIF \ ****************************************************************************** \ \ Name: menuKeysSup \ Type: Variable \ Category: Keyboard \ Summary: Negative inkey values for the menu keys (SPACE, "1", "2" and "3") \ for the Superior Software release \ \ ****************************************************************************** IF _ACORNSOFT OR _4TRACKS EQUB &DD, &EE \ These values are workspace noise and have no meaning EQUB &77, &BB ELIF _SUPERIOR OR _REVSPLUS .menuKeysSup EQUB &9D \ Negative inkey value for SPACE EQUB &CF \ Negative inkey value for "1" EQUB &CE \ Negative inkey value for "2" EQUB &EE \ Negative inkey value for "3" ENDIF \ ****************************************************************************** \ \ Name: totalPointsHi \ Type: Variable \ Category: Drivers \ Summary: High byte of total accumulated points for each driver \ \ ------------------------------------------------------------------------------ \ \ Indexed by driver number (0 to 19). \ \ Gets set in InitialiseDrivers. \ \ Stored as a 24-bit value (totalPointsTop totalPointsHi totalPointsLo). \ \ ****************************************************************************** .totalPointsHi EQUB &DD, &EE \ These values are workspace noise and have no meaning EQUB 0, 0, 0, 0, 0, 0 EQUB 0, 0, 0, 0, 0, 0 EQUB 0, 0, 0, 0, 0, 0 \ ****************************************************************************** \ \ Name: racePointsHi \ Type: Variable \ Category: Drivers \ Summary: High byte of race points calculated for each position \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (racePointsHi racePointsLo). \ \ ****************************************************************************** .racePointsHi EQUB &77, &BB \ These values are workspace noise and have no meaning EQUB &DD, &EE EQUB &77, &BB EQUB &DD, &EE \ ****************************************************************************** \ \ Name: token30 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 30 \ Deep dive: Text tokens \ \ ****************************************************************************** .token30 EQUB 31, 5, 24 \ Move text cursor to column 5, row 24 EQUB 134 \ Set foreground colour to cyan alphanumeric EQUB 200 + 17 \ Print token 17 ("PRESS ") EQUS "SPACE BAR " \ Print "SPACE BAR " EQUS "TO CONTINUE" \ Print "TO CONTINUE" EQUB 255 \ End token EQUB &45, &FF \ These bytes appear to be unused \ ****************************************************************************** \ \ Name: token2 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 2 \ Deep dive: Text tokens \ \ ****************************************************************************** .token2 EQUS "GRID POSITIONS" \ Print "GRID POSITIONS" EQUB 255 \ End token \ ****************************************************************************** \ \ Name: dashData20 \ Type: Variable \ Category: Screen buffer \ Summary: Contains code that gets moved into screen memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData20 SKIP 36 \ Populated with code from &7BAA to &7BCD \ ****************************************************************************** \ \ Name: PrintHeaderChecks \ Type: Subroutine \ Category: Text \ Summary: Print chequered lines above and below the header \ \ ****************************************************************************** .PrintHeaderChecks LDY #1 \ We are about to print two chequered lines, so set a \ line counter in Y .head1 LDA endChecks,Y \ Set T to the screen address offset of the end of the STA T \ Y-th chequered line LDX startChecks,Y \ Set A to the screen address offset of the start of the \ Y-th chequered line LDA #151 \ Poke the "white graphics" character into the X-th byte STA row2_column1,X \ of screen memory at column 1, row 2, so the subsequent \ bytes are shown as graphics characters LDA #226 \ Set A to the graphics character with the top-right and \ bottom-right blocks set to white, to form the first \ character of the line (i.e. the first two checks in \ the chequered line) .head2 INX \ Increment the screen address offset to move along one \ character STA row2_column1,X \ Poke character A into screen memory LDA #230 \ Set A to the graphics character with the top-right, \ bottom-right and centre-left blocks set to white, to \ form the rest of the checks on the chequered line CPX T \ Loop back to print the next character in the line BNE head2 \ until we have reached the screen address in T DEY \ Decrement the line counter BPL head1 \ Loop back to print the second line RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: startChecks \ Type: Variable \ Category: Text \ Summary: The screen address offset of the start of each chequered header \ line \ \ ****************************************************************************** .startChecks EQUB 0 \ Start the first line at row 2, column 1 (as the offset \ is added to row2_column1) EQUB 40 * 3 \ Start the second line on row 5, column 1 (as there are \ 40 characters per row) \ ****************************************************************************** \ \ Name: endChecks \ Type: Variable \ Category: Text \ Summary: The screen address offset of the end of each chequered header line \ \ ****************************************************************************** .endChecks EQUB 35 \ End the first line after 35 characters EQUB 35 + (40 * 3) \ End the first line after 35 characters \ ****************************************************************************** \ \ Name: token53 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 53 \ Deep dive: Text tokens \ \ ****************************************************************************** .token53 EQUB 160 + 15 \ Print 15 spaces EQUS "FINISHED" \ Print "FINISHED" EQUB 160 + 15 \ Print 15 spaces EQUB 255 \ End token EQUB &00, &00 \ These bytes appear to be unused \ ****************************************************************************** \ \ Name: token41 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 41 \ Deep dive: Text tokens \ \ ****************************************************************************** .token41 EQUB 160 + 6 \ Print 6 spaces EQUS "Less than one " \ Print "Less than one " EQUS "minute to go" \ Print "minute to go" EQUB 160 + 6 \ Print 6 spaces EQUB 255 \ End token \ ****************************************************************************** \ \ Name: token38 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 38 \ Deep dive: Text tokens \ \ ****************************************************************************** .token38 EQUB 31, 5, 20 \ Move text cursor to column 5, row 20 EQUB 132, 157 \ Set background colour to blue EQUB 134 \ Set foreground colour to cyan alphanumeric EQUS "3" \ Print "3" EQUB 160 + 2 \ Print 2 spaces EQUB 156 \ Set background colour to black EQUB 160 + 5 \ Print 5 spaces EQUB 131 \ Set foreground colour to yellow alphanumeric EQUB 255 \ End token EQUB &81, &81 \ These bytes appear to be unused EQUB &81 \ ****************************************************************************** \ \ Name: dashData21 \ Type: Variable \ Category: Screen buffer \ Summary: Contains code that gets moved into screen memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData21 SKIP 36 \ Populated with code from &7B86 to &7BA9 \ ****************************************************************************** \ \ Name: tokenLo \ Type: Variable \ Category: Text \ Summary: Low byte of the token address lookup table \ Deep dive: Text tokens \ \ ------------------------------------------------------------------------------ \ \ Note that token 47 is not used. \ \ ****************************************************************************** .tokenLo EQUB LO(token0) EQUB LO(token1) EQUB LO(token2) EQUB LO(token3) EQUB LO(token4) EQUB LO(token5) EQUB LO(token6) EQUB LO(token7) EQUB LO(token8) EQUB LO(token9) EQUB LO(token10) EQUB LO(token11) EQUB LO(token12) EQUB LO(token13) EQUB LO(token14) EQUB LO(token15) EQUB LO(token16) EQUB LO(token17) EQUB LO(token18) EQUB LO(token19) EQUB LO(token20) EQUB LO(token21) EQUB LO(token22) EQUB LO(token23) EQUB LO(token24) EQUB LO(token25) EQUB LO(token26) EQUB LO(token27) EQUB LO(token28) EQUB LO(token29) EQUB LO(token30) EQUB LO(token31) EQUB LO(token32) EQUB LO(token33) EQUB LO(token34) EQUB LO(token35) EQUB LO(token36) EQUB LO(token37) EQUB LO(token38) EQUB LO(token39) EQUB LO(token40) EQUB LO(token41) EQUB LO(token42) EQUB LO(token43) EQUB LO(token44) EQUB LO(token45) EQUB LO(token46) EQUB 0 EQUB LO(token48) EQUB LO(token49) EQUB LO(token50) EQUB LO(token51) EQUB LO(token52) EQUB LO(token53) \ ****************************************************************************** \ \ Name: yLookupHi \ Type: Variable \ Category: Drawing pixels \ Summary: Lookup table for converting pixel y-coordinate to high byte of \ screen address \ \ ------------------------------------------------------------------------------ \ \ This table returns the high byte of the screen address of the start of the \ row, for the custom screen mode. \ \ Note that the custom screen mode starts at address &5A80, so the first two \ entries in this table do not point to screen memory; the first two character \ rows in this table are off the top of the custom screen, so the first row \ on-screen is the third row. This is why, when we print the top two lines of \ text in the custom screen with the PrintCharacter routine, we do so at the \ following y-coordinates: \ \ * yCursor = 24 for the first line of text \ \ * yCursor = 33 for the second line of text \ \ To see where these are on-screen, we need to subtract 16 for the first two \ character rows which are off the top of the screen, to give: \ \ * y-coordinate = 8 for the first line of text \ \ * y-coordinate = 17 for the second line of text \ \ The value passed to PrintCharacter points to the bottom row of the character \ to print, so the first coordinate points to the ninth pixel row (as the first \ pixel row is row 0), and the second points to the 18th pixel row. There are \ eight pixels in each character row, so this prints the first row of text so \ that it has a one-pixel margin between the top of the text and the top of the \ screen, and it prints the second row so that it has a one-pixel margin between \ the top of the text and the bottom of the line above. \ \ I don't know why this table starts at &5800 and not &5A80, but that's how it \ is. \ \ ****************************************************************************** .yLookupHi FOR I%, 0, 31 EQUB HI(&5800 + (I% * &140)) NEXT \ ****************************************************************************** \ \ Name: mirrorAddressLo \ Type: Variable \ Category: Dashboard \ Summary: The low byte of the base screen address of each mirror segment \ Deep dive: Wing mirrors \ \ ****************************************************************************** .mirrorAddressLo EQUB LO(mirror0) \ Mirror segment 0 (left mirror, outer segment) EQUB LO(mirror1) \ Mirror segment 1 (left mirror, middle segment) EQUB LO(mirror2) \ Mirror segment 2 (left mirror, inner segment) EQUB LO(mirror3) \ Mirror segment 3 (right mirror, inner segment) EQUB LO(mirror4) \ Mirror segment 4 (right mirror, middle segment) EQUB LO(mirror5) \ Mirror segment 5 (right mirror, outer segment) \ ****************************************************************************** \ \ Name: dashData22 \ Type: Variable \ Category: Screen buffer \ Summary: Contains code that gets moved into screen memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData22 SKIP 36 \ Populated with code from &7B62 to &7B85 \ ****************************************************************************** \ \ Name: tokenHi \ Type: Variable \ Category: Text \ Summary: high byte of the token address lookup table \ Deep dive: Text tokens \ \ ------------------------------------------------------------------------------ \ \ Note that token 47 is not used. \ \ ****************************************************************************** .tokenHi EQUB HI(token0) EQUB HI(token1) EQUB HI(token2) EQUB HI(token3) EQUB HI(token4) EQUB HI(token5) EQUB HI(token6) EQUB HI(token7) EQUB HI(token8) EQUB HI(token9) EQUB HI(token10) EQUB HI(token11) EQUB HI(token12) EQUB HI(token13) EQUB HI(token14) EQUB HI(token15) EQUB HI(token16) EQUB HI(token17) EQUB HI(token18) EQUB HI(token19) EQUB HI(token20) EQUB HI(token21) EQUB HI(token22) EQUB HI(token23) EQUB HI(token24) EQUB HI(token25) EQUB HI(token26) EQUB HI(token27) EQUB HI(token28) EQUB HI(token29) EQUB HI(token30) EQUB HI(token31) EQUB HI(token32) EQUB HI(token33) EQUB HI(token34) EQUB HI(token35) EQUB HI(token36) EQUB HI(token37) EQUB HI(token38) EQUB HI(token39) EQUB HI(token40) EQUB HI(token41) EQUB HI(token42) EQUB HI(token43) EQUB HI(token44) EQUB HI(token45) EQUB HI(token46) EQUB 0 EQUB HI(token48) EQUB HI(token49) EQUB HI(token50) EQUB HI(token51) EQUB HI(token52) EQUB HI(token53) \ ****************************************************************************** \ \ Name: shortAxis \ Type: Variable \ Category: Dashboard \ Summary: Code modifications for the DrawDashboardLine line-drawing routine \ \ ------------------------------------------------------------------------------ \ \ When drawing a line, the short axis is only stepped along when the slope error \ adds up to a whole pixel, so this steps along the shorter axis of the line's \ vector. See the DrawDashboardLine routine for details. \ \ ****************************************************************************** .shortAxis INX \ V = 0, INX and DEY = Steep slope, right and up DEY \ V = 1, DEY and INX = Shallow slope, right and up INY \ V = 2, INY and INX = Shallow slope, right and down INX \ V = 3, INX and INY = Steep slope, right and down DEX \ V = 4, DEX and INY = Steep slope, left and down INY \ V = 5, INY and DEX DEY \ V = 6, DEY and DEX DEX \ V = 7, DEX and DEY \ ****************************************************************************** \ \ Name: stepAxis \ Type: Variable \ Category: Dashboard \ Summary: Code modifications for the DrawDashboardLine line-drawing routine \ \ ------------------------------------------------------------------------------ \ \ When drawing a line, we step along the longer axis of the line's vector by one \ pixel for loop around the line-drawing routine. See the DrawDashboardLine \ routine for details. \ \ ****************************************************************************** .stepAxis DEY \ V = 0, INX and DEY = Steep slope, right and up INX \ V = 1, DEY and INX = Shallow slope, right and up INX \ V = 2, INY and INX = Shallow slope, right and down INY \ V = 3, INX and INY = Steep slope, right and down INY \ V = 4, DEX and INY = Steep slope, left and down DEX \ V = 5, INY and DEX DEX \ V = 6, DEY and DEX DEY \ V = 7, DEX and DEY EQUB &18, &EA EQUB &EA, &18 EQUB &18, &EA EQUB &EA, &18 \ ****************************************************************************** \ \ Name: mirrorAddressHi \ Type: Variable \ Category: Dashboard \ Summary: The high byte of the base screen address of each mirror segment \ Deep dive: Wing mirrors \ \ ****************************************************************************** .mirrorAddressHi EQUB HI(mirror0) \ Mirror segment 0 (left mirror, outer segment) EQUB HI(mirror1) \ Mirror segment 1 (left mirror, middle segment) EQUB HI(mirror2) \ Mirror segment 2 (left mirror, inner segment) EQUB HI(mirror3) \ Mirror segment 3 (right mirror, inner segment) EQUB HI(mirror4) \ Mirror segment 4 (right mirror, middle segment) EQUB HI(mirror5) \ Mirror segment 5 (right mirror, outer segment) \ ****************************************************************************** \ \ Name: mirrorSegment \ Type: Variable \ Category: Dashboard \ Summary: Lookup table for working out which mirror segment a car should \ appear in \ Deep dive: Wing mirrors \ \ ****************************************************************************** .mirrorSegment EQUB &90 / 8 \ Mirror segment 0 (left mirror, outer segment) \ So -14.5 <= yaw < -13.5 (as &90 / 8 = -112 / 8 = -14) EQUB &88 / 8 \ Mirror segment 1 (left mirror, middle segment) \ So -15.5 <= yaw < -14.5 (as &88 / 8 = -120 / 8 = -15) EQUB &80 / 8 \ Mirror segment 2 (left mirror, inner segment) \ So -16.5 <= yaw < -15.5 (as &80 / 8 = -128 / 8 = -16) EQUB 14 \ Mirror segment 3 (right mirror, inner segment) \ So 14.5 <= yaw < 15.5 EQUB 13 \ Mirror segment 4 (right mirror, middle segment) \ So 13.5 <= yaw < 14.5 EQUB 12 \ Mirror segment 5 (right mirror, outer segment) \ So 12.5 <= yaw < 13.5 EQUB &81, &81 \ These bytes appear to be unused \ ****************************************************************************** \ \ Name: dashData23 \ Type: Variable \ Category: Screen buffer \ Summary: Contains code that gets moved into screen memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData23 SKIP 36 \ Populated with code from &7B3E to &7B61 \ ****************************************************************************** \ \ Name: xHeader \ Type: Variable \ Category: Text \ Summary: Column number for printing mode 7 headers \ \ ------------------------------------------------------------------------------ \ \ The values in this table are used by the PrintHeader routine to print out \ headers in mode 7. \ \ ****************************************************************************** .xHeader EQUB 4 EQUB 7 EQUB 9 EQUB 7 EQUB 0 EQUB 3 + 8 EQUB 7 \ ****************************************************************************** \ \ Name: yHeader \ Type: Variable \ Category: Text \ Summary: Row number for printing mode 7 headers \ \ ------------------------------------------------------------------------------ \ \ The values in this table are used by the PrintHeader routine to print out \ headers in mode 7. \ \ ****************************************************************************** .yHeader EQUB 3 EQUB 0 EQUB 0 EQUB 0 EQUB 4 EQUB 4 EQUB 0 \ ****************************************************************************** \ \ Name: headerSpaces \ Type: Variable \ Category: Text \ Summary: Number of spaces for printing mode 7 headers \ \ ------------------------------------------------------------------------------ \ \ The values in this table are used by the PrintHeader routine to print out \ headers in mode 7. \ \ ****************************************************************************** .headerSpaces EQUB 160 + 10 EQUB 160 + 15 EQUB 160 + 19 EQUB 160 + 15 EQUB 160 + 2 EQUB 160 + 24 EQUB 160 + 15 \ ****************************************************************************** \ \ Name: headerBackground \ Type: Variable \ Category: Text \ Summary: Background colour for printing mode 7 headers \ \ ------------------------------------------------------------------------------ \ \ The values in this table are used by the PrintHeader routine to print out \ headers in mode 7. \ \ ****************************************************************************** .headerBackground EQUB 129 EQUB 129 EQUB 133 EQUB 132 EQUB 163 EQUB 131 EQUB 133 \ ****************************************************************************** \ \ Name: headerForeground \ Type: Variable \ Category: Text \ Summary: Foreground colour for printing mode 7 headers \ \ ------------------------------------------------------------------------------ \ \ The values in this table are used by the PrintHeader routine to print out \ headers in mode 7. \ \ ****************************************************************************** .headerForeground EQUB 131 EQUB 131 EQUB 135 EQUB 135 EQUB 127 EQUB 132 EQUB 135 \ ****************************************************************************** \ \ Name: token46 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 46 \ Deep dive: Text tokens \ \ ****************************************************************************** .token46 EQUB 22, 7 \ Switch to screen mode 7 EQUB 23, 0, 10, 32 \ Disable cursor EQUB 0, 0, 0 EQUB 0, 0, 0 EQUB 255 \ End token \ ****************************************************************************** \ \ Name: token24 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 24 \ Deep dive: Text tokens \ \ ****************************************************************************** .token24 EQUB 200 + 35 \ Print token 35 (cyan, move cursor to prompt position) EQUB 200 + 10 \ Print token 10 ("SELECT ") EQUS "WING SETTINGS" \ Print "WING SETTINGS" EQUB 200 + 16 \ Print token 16 (" > ") EQUS "range 0 to 40" \ Print "range 0 to 40" EQUB 31, 14, 16 \ Move text cursor to column 14, row 16 EQUS "rear" \ Print "rear" EQUB 160 + 2 \ Print 2 spaces EQUB 133 \ Set foreground colour to magenta alphanumeric EQUB 200 + 16 \ Print token 16 (" > ") EQUB 255 \ End token EQUB &81, &81 \ These bytes appear to be unused EQUB &81, &81 \ ****************************************************************************** \ \ Name: dashData24 \ Type: Variable \ Category: Screen buffer \ Summary: Contains code that gets moved into screen memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData24 SKIP 36 \ Populated with code from &7B1A to &7B3D \ ****************************************************************************** \ \ Name: GetWingSettings \ Type: Subroutine \ Category: Keyboard \ Summary: Get the front and rear wing settings from the player \ \ ****************************************************************************** .GetWingSettings LDX #5 \ Print "THE PITS" as a double-height header at column JSR PrintHeader \ 11, row 4, in blue text on a yellow background LDX #24 \ Print token 24, which shows the prompt "SELECT WING JSR PrintToken \ SETTINGS > range 0 to 40" and a further prompt of \ "rear > " JSR GetNumberInput \ Fetch a number from the keyboard STA rearWingSetting \ Store the entered number in rearWingSetting LDX #25 \ Print token 25, which shows a prompt of "front > " JSR PrintToken JSR GetNumberInput \ Fetch a number from the keyboard STA frontWingSetting \ Store the entered number in frontWingSetting JSR WaitForSpace \ Print a prompt and wait for SPACE to be pressed RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: PrintRaceClass \ Type: Subroutine \ Category: Text \ Summary: Print the race class \ \ ****************************************************************************** .PrintRaceClass LDA raceClass \ Set A to the race class + 7, so that gives us: CLC \ ADC #7 \ * 7 for Novice TAX \ * 8 for Amateur \ * 9 for Professional JSR PrintToken \ Print token X, which will be token 7 ("Novice"), token \ 8 ("Amateur") or token 9 ("Professional") RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: token50 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 50 \ Deep dive: Text tokens \ \ ****************************************************************************** .token50 EQUB 31, 24, 2 \ Move text cursor to column 24, row 2 EQUB 200 + 18 \ Print token (configurable token number, default is 18, \ which is " 5") EQUB 200 + 14 \ Print token 14 (" laps") EQUB 255 \ End token \ ****************************************************************************** \ \ Name: token6 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 6 \ Deep dive: Text tokens \ \ ****************************************************************************** .token6 EQUB 160 + 2 \ Print 2 spaces EQUS "BEST LAP TIMES" \ Print "BEST LAP TIMES" EQUB 160 + 2 \ Print 2 spaces EQUB 255 \ End token \ ****************************************************************************** \ \ Name: token13 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 13 \ Deep dive: Text tokens \ \ ****************************************************************************** .token13 EQUS " mins" \ Print " mins" EQUB 255 \ End token \ ****************************************************************************** \ \ Name: token14 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 14 \ Deep dive: Text tokens \ \ ****************************************************************************** .token14 EQUS " laps" \ Print " laps" EQUB 255 \ End token \ ****************************************************************************** \ \ Name: token15 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 15 \ Deep dive: Text tokens \ \ ****************************************************************************** .token15 EQUS " RACE" \ Print " RACE" EQUB 255 \ End token EQUS "ins" \ These bytes appear to be unused EQUB &FF, &81 EQUB &81, &81 EQUB &81, &81 \ ****************************************************************************** \ \ Name: dashData25 \ Type: Variable \ Category: Screen buffer \ Summary: Contains code and part of the dashboard image that gets moved into \ screen memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData25 SKIP 10 \ Populated with part of the dashboard image SKIP 26 \ Populated with code from &7B00 to &7B19 \ ****************************************************************************** \ \ Name: objectIndex \ Type: Variable \ Category: 3D objects \ Summary: Index range of an object's data in the object data tables \ Deep dive: Object definitions \ \ ------------------------------------------------------------------------------ \ \ Given an object type, this table contains the index range for the object's \ data in the objectTop, objectBottom, objectLeft, objectRight and objectColour \ tables. \ \ ****************************************************************************** .objectIndex EQUB 0 \ Object type 0 = 0 to 4 EQUB 5 \ Object type 1 = 5 to 8 EQUB 9 \ Object type 2 = 9 to 13 EQUB 14 \ Object type 3 = 14 to 17 EQUB 18 \ Object type 4 = 18 to 24 EQUB 25 \ Object type 5 = 25 EQUB 26 \ Object type 6 = 26 EQUB 27 \ Object type 7 = 27 to 29 EQUB 30 \ Object type 8 = 30 to 31 EQUB 32 \ Object type 9 = 32 to 33 EQUB 34 \ Object type 10 = 34 to 36 EQUB 37 \ Object type 11 = 37 to 38 EQUB 39 \ Object type 12 = 39 to 40 \ ****************************************************************************** \ \ Name: scaffoldIndex \ Type: Variable \ Category: 3D objects \ Summary: Index of an object's scaffold in the objectScaffold table \ Deep dive: Scaling objects with scaffolds \ \ ------------------------------------------------------------------------------ \ \ Given an object type, this table contains the index range for the object's \ scaffold in the objectScaffold table. \ \ ****************************************************************************** .scaffoldIndex EQUB 0 \ Object type 0 = 0 to 7 EQUB 8 \ Object type 1 = 8 to 15 EQUB 16 \ Object type 2 = 16 to 23 EQUB 24 \ Object type 3 = 24 to 29 EQUB 30 \ Object type 4 = 30 to 37 EQUB 38 \ Object type 5 = 38 to 40 EQUB 41 \ Object type 6 = 41 to 43 EQUB 44 \ Object type 7 = 44 to 48 EQUB 49 \ Object type 8 = 49 to 52 EQUB 53 \ Object type 9 = 53 to 56 EQUB 57 \ Object type 10 = 57 to 61 EQUB 62 \ Object type 11 = 62 to 65 EQUB 66 \ Object type 12 = 66 to 69 EQUB 70 \ ****************************************************************************** \ \ Name: GetDriverAddress \ Type: Subroutine \ Category: Text \ Summary: Get the address of the specified driver's name \ \ ------------------------------------------------------------------------------ \ \ This routine calculates the address of driver A's name using the following: \ \ (Y A) = driverNames1 + (A div 4) * &100 + (A mod 4) * 12 \ \ The names of the 20 drivers are stored in in five blocks within the main game \ code. Each block of contains four names, each of which is 12 characters long. \ The blocks start every &100 (256) bytes, starting with the first block at \ driverNames1 and going through to driverNames5. \ \ Given driver number A, A div 4 is the block number, while A mod 4 is the \ number of the 12-character name within that block. So we have: \ \ * (A div 4) * &100 is the address of the start of the block containing the \ name of driver A \ \ * (A mod 4) * 12 is the offset of driver A's 12-character name within that \ block \ \ The first block is at driverNames1, so we add them together to arrive at the \ calculation above. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The driver number (0 to 19) \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ (Y A) The address of the driver's 12-character name \ \ ****************************************************************************** .GetDriverAddress TXA \ We start with the high byte of the calculation, which LSR A \ is Y = HI(driverNames1) + (A div 4) LSR A CLC ADC #HI(driverNames1) TAY \ And now we do the low byte calculation, which is \ A = LO(driverNames1) + (A mod 4) * 12 TXA \ Set A = (A mod 4) * 4 AND #3 ASL A ASL A STA T \ Set T = A \ = (A mod 4) * 4 ASL A \ Set A = (A mod 4) * 8 CLC \ Set A = A + T ADC T \ = (A mod 4) * 8 + (A mod 4) * 4 \ = (A mod 4) * 12 ADC #LO(driverNames1) \ Set A = LO(driverNames1) + A \ = LO(driverNames1) + (A mod 4) * 12 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: token27 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 27 \ Deep dive: Text tokens \ \ ****************************************************************************** .token27 EQUB 200 + 54 \ Print token 54 ("FORMULA 3 CHAMPIONSHIP" header) EQUB 200 + 36 \ Print token 36 (menu option 1 with "PRESS" prompt) EQUB 200 + 11 \ Print token 11 ("ENTER ") EQUS "ANOTHER" \ Print "ANOTHER" EQUB 200 + 12 \ Print token 12 (" DRIVER") EQUB 200 + 37 \ Print token 37 (menu option 2) EQUS "START" \ Print "START" EQUB 200 + 15 \ Print token 15 (" RACE") EQUB 255 \ End token \ ****************************************************************************** \ \ Name: token34 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 34 \ \ ------------------------------------------------------------------------------ \ \ The configurable values below are set in the PrintHeader routine. \ Deep dive: Text tokens \ \ ****************************************************************************** .token34 EQUB 141 \ Set double-height text EQUB 129, 157 \ Set background colour (configurable, default is red) EQUB 131 \ Set foreground colour (configurable, default is yellow \ alphanumeric) EQUB 200+0 \ Print token (configurable token number, default is 0, \ which is "FORMULA 3 CHAMPIONSHIP") EQUB 160+2 \ Print spaces (configurable, default is 2 spaces) EQUB 156 \ Set background colour to black EQUB 255 \ End token EQUB &81, &81 \ These bytes appear to be unused EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 \ ****************************************************************************** \ \ Name: dashData26 \ Type: Variable \ Category: Screen buffer \ Summary: Contains part of the dashboard image that gets moved into screen \ memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData26 SKIP 41 \ ****************************************************************************** \ \ Name: PrintSpaces \ Type: Subroutine \ Category: Text \ Summary: Print the specified number of spaces \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ A The number of spaces to print (1 to 39) \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ Z flag Set (so a BEQ following the routine call will always \ branch) \ \ ****************************************************************************** .PrintSpaces STA T \ Set T to the number of spaces to print to use as a \ loop counter .spac1 LDA #' ' \ Print a space JSR PrintCharacter DEC T \ Decrement the loop counter BNE spac1 \ Loop back until we have printed the right number of \ spaces RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: DrawFence (Part 1 of 2) \ Type: Subroutine \ Category: Screen buffer \ Summary: Draw the fence that we crash into when running off the track \ \ ------------------------------------------------------------------------------ \ \ Other entry points: \ \ DrawFence-1 Contains an RTS \ \ ****************************************************************************** .DrawFence LDA #0 \ Set playerMoving = 0 to denote that the player's car STA playerMoving \ is not moving STA T \ Set T = 0, to use as a counter as we work our way \ through the 40 dash data blocks that contain the track \ view STA P \ Set (Q P) to point to the first dash data block at LDA #HI(dashData) \ dashData (this works because the low byte of dashData STA Q \ is zero) .fenc1 LDX T \ If X = 40 then we have drawn the fence across all the CPX #40 \ dash data blocks, so return from the subroutine (as BEQ DrawFence-1 \ DrawFence-1 contains an RTS) LDA dashDataOffset,X \ Set U to the dashDataOffset for the current dash data STA U \ block LDY #70 \ Set Y = 70, to use as a loop counter that works \ through all 70 bytes in the dash data block JMP fenc2 \ We now jump to part 2 to work our way through the 70 \ bytes in the dash data block, each of which represents \ a four-pixel line, with 70 lines stacked one on top of \ the other, in a four-pixel-wide vertical strip \ ****************************************************************************** \ \ Name: fencePixelsGrass \ Type: Variable \ Category: Screen buffer \ Summary: Pixel bytes for the fence with green grass behind it \ \ ****************************************************************************** .fencePixelsGrass EQUB %10101010 \ Four pixels: green, black, green, black EQUB %01110111 \ Four pixels: black, green, green, green EQUB %10101010 \ Four pixels: green, black, green, black EQUB %11011101 \ Four pixels: green, green, black, green \ ****************************************************************************** \ \ Name: fencePixelsSky \ Type: Variable \ Category: Screen buffer \ Summary: Pixel bytes for the fence with blue sky behind it \ \ ****************************************************************************** .fencePixelsSky EQUB %00001010 \ Four pixels: blue, black, blue, black EQUB %00000111 \ Four pixels: black, blue, blue, blue EQUB %00001010 \ Four pixels: blue, black, blue, black EQUB %00001101 \ Four pixels: blue, blue, black, blue \ ****************************************************************************** \ \ Name: token21 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 21 \ Deep dive: Text tokens \ \ ****************************************************************************** .token21 EQUB 200 + 54 \ Print token 54 ("FORMULA 3 CHAMPIONSHIP" header) EQUB 200 + 36 \ Print token 36 (menu option 1 with "PRESS" prompt) EQUB 200 + 7 \ Print token 7 ("Novice") EQUB 200 + 37 \ Print token 37 (menu option 2) EQUB 200 + 8 \ Print token 8 ("Amateur") EQUB 200 + 38 \ Print token 38 (menu option 3) EQUB 200 + 9 \ Print token 9 ("Professional") EQUB 200 + 35 \ Print token 35 (cyan, move cursor to prompt position) EQUB 160 + 4 \ Print 4 spaces EQUB 200 + 10 \ Print token 10 ("SELECT ") EQUS "THE CLASS OF" \ Print "THE CLASS OF" EQUB 200 + 15 \ Print token 15 (" RACE") EQUB 255 \ End token EQUB &81, &81 \ These bytes appear to be unused EQUB &81, &81 \ ****************************************************************************** \ \ Name: dashData27 \ Type: Variable \ Category: Screen buffer \ Summary: Contains part of the dashboard image that gets moved into screen \ memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData27 SKIP 52 \ ****************************************************************************** \ \ Name: segmentStep \ Type: Variable \ Category: Track geometry \ Summary: The number of segments we step over when working backwards through \ the track segment buffer in GetSegmentAngles \ \ ****************************************************************************** .segmentStep EQUB 0 \ Not used as segmentStep is only accessed with indexes \ greater than zero, but this represents starting at \ the front segment in the track segment buffer, so we \ step backwards through the buffer \ \ Entry 32 in the track segment buffer is the player's \ car (as it is 32 segments behind the front segment), \ so the player's car is at entry 13 in the track \ segment list EQUB 13 * 3 \ Step back 13 segments to 13 for segment list entry 1 EQUB 6 * 3 \ Step back 6 segments to 19 for segment list entry 2 EQUB 3 * 3 \ Step back 3 segments to 22 for segment list entry 3 EQUB 3 \ Step back 1 segment to 23 for segment list entry 4 EQUB 3 \ Step back 1 segment to 24 for segment list entry 5 EQUB 3 \ Step back 1 segment to 25 for segment list entry 6 EQUB 3 \ Step back 1 segment to 26 for segment list entry 7 EQUB 3 \ Step back 1 segment to 27 for segment list entry 8 EQUB 3 \ Step back 1 segment to 28 for segment list entry 9 EQUB 3 \ Step back 1 segment to 29 for segment list entry 10 EQUB 3 \ Step back 1 segment to 30 for segment list entry 11 EQUB 3 \ Step back 1 segment to 31 for segment list entry 12 EQUB 3 \ Step back 1 segment to 32 for segment list entry 13 EQUB 3 \ Step back 1 segment to 33 for segment list entry 14 EQUB 3 \ Step back 1 segment to 34 for segment list entry 15 EQUB 3 \ Step back 1 segment to 35 for segment list entry 16 EQUB 3 \ Not used as there is a maximum of 16 segments in the \ track segment list \ ****************************************************************************** \ \ Name: shiftedKeys \ Type: Variable \ Category: Keyboard \ Summary: Negative inkey values for the configuration keys that are pressed \ in combination with SHIFT \ \ ****************************************************************************** .shiftedKeys EQUB &86 \ Right arrow EQUB &8E \ f1 EQUB &8D \ f2 EQUB &8D \ f2 EQUB &EB \ f4 EQUB &8B \ f5 EQUB &DF \ f0 EQUB &96 \ COPY EQUB &A6 \ DELETE EQUB &E9 \ f7 IF _SUPERIOR OR _REVSPLUS EQUB &8C \ f3 EQUB &8A \ f6 ENDIF \ ****************************************************************************** \ \ Name: menuKeys \ Type: Variable \ Category: Keyboard \ Summary: Negative inkey values for the menu keys (SPACE, "1", "2" and "3") \ for the Acornsoft release \ \ ****************************************************************************** IF _ACORNSOFT OR _4TRACKS .menuKeys EQUB &9D \ Negative inkey value for SPACE EQUB &CF \ Negative inkey value for "1" EQUB &CE \ Negative inkey value for "2" EQUB &EE \ Negative inkey value for "3" ELIF _SUPERIOR OR _REVSPLUS EQUB &CE, &EE \ These bytes appear to be unused ENDIF \ ****************************************************************************** \ \ Name: timeFromOption \ Type: Variable \ Category: Keyboard \ Summary: Table to convert from the option numbers in the qualifying lap \ duration menu to the actual number of minutes \ \ ------------------------------------------------------------------------------ \ \ Interestingly, the menu offers 5, 10 and 20 minutes, but these translate into \ 5, 10 and 26 minutes of actual qualifying time. \ \ ****************************************************************************** .timeFromOption EQUB 4, 9, 25 EQUB &00 \ This byte appears to be unused \ ****************************************************************************** \ \ Name: lapsFromOption \ Type: Variable \ Category: Keyboard \ Summary: Table to convert from the option numbers in the laps menu to the \ actual number of laps \ \ ****************************************************************************** .lapsFromOption EQUB 5, 10, 20 \ ****************************************************************************** \ \ Name: pointsForPlace \ Type: Variable \ Category: Drivers \ Summary: The points awarded for the top six places, plus the fastest lap \ \ ****************************************************************************** .pointsForPlace EQUB 9 \ Points for first place EQUB 6 \ Points for second place EQUB 4 \ Points for third place EQUB 3 \ Points for fourth place EQUB 2 \ Points for fifth place EQUB 1 \ Points for sixth place EQUB 1 \ Points for the fastest lap EQUB &00, &00 \ These bytes appear to be unused \ ****************************************************************************** \ \ Name: token29 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 29 \ Deep dive: Text tokens \ \ ****************************************************************************** .token29 EQUB 200 + 54 \ Print token 54 ("FORMULA 3 CHAMPIONSHIP" header) EQUB 200 + 35 \ Print token 35 (cyan, move cursor to prompt position) EQUB 160 + 5 \ Print 5 spaces EQUB 130 \ Set foreground colour to green alphanumeric EQUB 200 + 12 \ Print token 12 (" DRIVER") EQUB 200 + 16 \ Print token 16 (" > ") EQUB 255 \ End token \ ****************************************************************************** \ \ Name: token9 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 9 \ Deep dive: Text tokens \ \ ****************************************************************************** .token9 EQUS "Professional" \ Print "Professional" EQUB 255 \ End token EQUB &81, &81 \ These bytes appear to be unused EQUB &81, &81 \ ****************************************************************************** \ \ Name: dashData28 \ Type: Variable \ Category: Screen buffer \ Summary: Contains part of the dashboard image that gets moved into screen \ memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData28 SKIP 56 \ ****************************************************************************** \ \ Name: jumpShallowRight \ Type: Variable \ Category: Drawing the track \ Summary: Branch labels for self-modifying code in the DrawShallowToRight \ routine \ Deep dive: Drawing the track verges \ \ ****************************************************************************** .jumpShallowRight EQUB shlr4 - shlr2 EQUB shlr6 - shlr2 EQUB shlr8 - shlr2 EQUB shlr10 - shlr2 EQUB shlr13 - shlr2 EQUB shlr15 - shlr2 EQUB shlr17 - shlr2 EQUB shlr19 - shlr2 EQUB shlr3 - shlr2 \ These are not used EQUB shlr5 - shlr2 EQUB shlr7 - shlr2 EQUB shlr9 - shlr2 EQUB shlr12 - shlr2 EQUB shlr14 - shlr2 EQUB shlr16 - shlr2 EQUB shlr18 - shlr2 \ ****************************************************************************** \ \ Name: SetRowColours \ Type: Subroutine \ Category: Text \ Summary: Set the foreground and background colours for a table row \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ Y Table row number \ \ colourScheme Colour scheme: 0, 4, 8 \ \ ****************************************************************************** .SetRowColours TYA \ Set X = colourScheme if Y is even AND #1 \ = colourScheme + 1 if Y is odd CLC \ ADC colourScheme \ So X is now one of the following, depending on the TAX \ colour scheme and whether the row number is even or \ odd: \ \ * Scheme 0: 0 (even) or 1 (odd) \ * Scheme 4: 4 (even) or 5 (odd) \ * Scheme 8: 8 (even) or 9 (odd) \ \ We now fetch the colour palette from the rowColours \ table using the following offsets: \ \ * Scheme 0: 0 on 2 (even) or 1 on 3 (odd) \ * Scheme 4: 4 on 6 (even) or 5 on 7 (odd) \ * Scheme 8: 8 on 10 (even) or 9 on 11 (odd) LDA rowColours,X \ Set the configurable foreground colour in token 31 to STA token31+5 \ the X-th entry in the rowColours table LDA rowColours+2,X \ Set the configurable background colour in token 31 to STA token31+3 \ the X+2-th entry in the rowColours table RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: rowColours \ Type: Variable \ Category: Text \ Summary: Three different palettes for displaying even-odd rows in tables \ \ ****************************************************************************** .rowColours EQUB 132, 133 \ Scheme 0: Even rows: 132 on 134 (blue on cyan) EQUB 134, 135 \ Odd rows: 134 on 135 (cyan on white) EQUB 129, 132 \ Scheme 4: Even rows: 129 on 132 (red on blue) EQUB 131, 130 \ Odd rows: 131 on 130 (yellow on green) EQUB 131, 132 \ Scheme 8: Even rows: 131 on 132 (yellow on blue) EQUB 129, 135 \ Odd rows: 129 on 135 (red on white) \ ****************************************************************************** \ \ Name: token10 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 10 \ Deep dive: Text tokens \ \ ****************************************************************************** .token10 EQUS "SELECT " \ Print "SELECT " EQUB 255 \ End token \ ****************************************************************************** \ \ Name: token12 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 12 \ Deep dive: Text tokens \ \ ****************************************************************************** .token12 EQUS " DRIVER" \ Print " DRIVER" EQUB 255 \ End token EQUB &81, &81 \ These bytes appear to be unused EQUB &81, &81 \ ****************************************************************************** \ \ Name: dashData29 \ Type: Variable \ Category: Screen buffer \ Summary: Contains part of the dashboard image that gets moved into screen \ memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData29 SKIP 60 \ ****************************************************************************** \ \ Name: jumpSteepRight \ Type: Variable \ Category: Drawing the track \ Summary: Branch labels for self-modifying code in the DrawSteepToRight \ routine \ Deep dive: Drawing the track verges \ \ ****************************************************************************** .jumpSteepRight EQUB stlr2 - stlr2 EQUB stlr3 - stlr2 EQUB stlr4 - stlr2 EQUB stlr5 - stlr2 EQUB stlr6 - stlr2 EQUB stlr7 - stlr2 EQUB stlr8 - stlr2 EQUB stlr9 - stlr2 \ ****************************************************************************** \ \ Name: jumpSteepLeft \ Type: Variable \ Category: Drawing the track \ Summary: Branch labels for self-modifying code in the DrawSteepToLeft \ routine \ Deep dive: Drawing the track verges \ \ ****************************************************************************** .jumpSteepLeft EQUB strl9 - strl2 EQUB strl8 - strl2 EQUB strl7 - strl2 EQUB strl6 - strl2 EQUB strl5 - strl2 EQUB strl4 - strl2 EQUB strl3 - strl2 EQUB strl2 - strl2 \ ****************************************************************************** \ \ Name: GetNumberInput \ Type: Subroutine \ Category: Keyboard \ Summary: Fetch a number between 0 and 40 from the keyboard \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ A The value of the number entered (0 to 40) \ \ ****************************************************************************** .GetNumberInput LDA #LO(T) \ Set (Y A) = T LDY #HI(T) LDX #2 \ Fetch a string of up to two characters from the JSR GetTextInput \ keyboard, store the characters at location T and U (as \ U = T + 1), and set Y to the number of characters \ entered JSR GetNumberFromText \ Convert the two-character input into a number in A, \ and report the conversion status in the C flag BCC numb2 \ If we got a valid number that is 40 or less then the C \ flag will be clear, so jump to numb2 to return from \ the subroutine .numb1 DEY \ Decrement the number of characters in the entered \ string, which is in Y BMI GetNumberInput \ If Y is now negative, then there are no characters \ left on-screen, so jump back to the start of the \ routine to try fetching another number LDA #127 \ Otherwise delete the last character shown on-screen by JSR OSWRCH \ printing a delete character (ASCII 127) JMP numb1 \ Jump back to numb1 to delete any other on-screen \ characters before starting again .numb2 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: startMirror \ Type: Variable \ Category: Dashboard \ Summary: The offset from mirrorAddress for the start of each mirror segment \ \ ****************************************************************************** .startMirror EQUB &AA \ Mirror segment 0 (left mirror, outer segment) EQUB &AC \ Mirror segment 1 (left mirror, middle segment) EQUB &B0 \ Mirror segment 2 (left mirror, inner segment) EQUB &B0 \ Mirror segment 3 (right mirror, inner segment) EQUB &AC \ Mirror segment 4 (right mirror, middle segment) EQUB &AA \ Mirror segment 5 (right mirror, outer segment) \ ****************************************************************************** \ \ Name: token37 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 37 \ Deep dive: Text tokens \ \ ****************************************************************************** .token37 EQUB 31, 5, 18 \ Move text cursor to column 5, row 18 EQUB 132, 157 \ Set background colour to blue EQUB 134 \ Set foreground colour to cyan alphanumeric EQUS "2" \ Print "2" EQUB 160 + 2 \ Print 2 spaces EQUB 156 \ Set background colour to black EQUB 160 + 5 \ Print 5 spaces EQUB 131 \ Set foreground colour to yellow alphanumeric EQUB 255 \ End token EQUB &81, &81 \ These bytes appear to be unused EQUB &81, &81 \ ****************************************************************************** \ \ Name: dashData30 \ Type: Variable \ Category: Screen buffer \ Summary: Contains part of the dashboard image that gets moved into screen \ memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData30 SKIP 64 \ ****************************************************************************** \ \ Name: fillDataOffset \ Type: Variable \ Category: Dashboard \ Summary: Dash data offsets, tweaked to give bottom line values that are \ compatible with the process of filling blocks to the left \ Deep dive: Creating objects from edges \ \ ------------------------------------------------------------------------------ \ \ Contains dash data offsets that are skewed so that dash data blocks whose left \ neighbours start lower down the screen than they do (i.e. they have a smaller \ offset) contain the left neighbour's offset rather than their own. \ \ This ensures that when filling blocks to the left of the current block, the \ value of the bottom line (i.e. the offset) is set to the bottom of the column \ being filled, rather than the smaller column to its right. \ \ ****************************************************************************** .fillDataOffset EQUB dashData0 - (dashData + &80 * 0) - 1 \ These match dashDataOffset EQUB dashData1 - (dashData + &80 * 1) - 1 EQUB dashData2 - (dashData + &80 * 2) - 1 EQUB dashData3 - (dashData + &80 * 3) - 1 EQUB dashData4 - (dashData + &80 * 4) - 1 EQUB dashData5 - (dashData + &80 * 5) - 1 EQUB dashData6 - (dashData + &80 * 6) - 1 EQUB dashData6 - (dashData + &80 * 6) - 1 \ dashData6 repeated, as block 7 \ is smaller than block 6 EQUB dashData7 - (dashData + &80 * 7) - 1 \ These are out by one, so the EQUB dashData8 - (dashData + &80 * 8) - 1 \ X-th entry is the offset for EQUB dashData9 - (dashData + &80 * 9) - 1 \ block X - 1 EQUB dashData10 - (dashData + &80 * 10) - 1 EQUB dashData11 - (dashData + &80 * 11) - 1 EQUB dashData12 - (dashData + &80 * 12) - 1 EQUB dashData13 - (dashData + &80 * 13) - 1 EQUB dashData14 - (dashData + &80 * 14) - 1 EQUB dashData15 - (dashData + &80 * 15) - 1 EQUB dashData16 - (dashData + &80 * 16) - 1 EQUB dashData17 - (dashData + &80 * 17) - 1 EQUB dashData18 - (dashData + &80 * 18) - 1 EQUB dashData19 - (dashData + &80 * 19) - 1 EQUB dashData20 - (dashData + &80 * 20) - 1 EQUB dashData21 - (dashData + &80 * 21) - 1 EQUB dashData22 - (dashData + &80 * 22) - 1 EQUB dashData23 - (dashData + &80 * 23) - 1 EQUB dashData24 - (dashData + &80 * 24) - 1 \ dashData25 skipped as block 26 \ is larger than block 25 EQUB dashData26 - (dashData + &80 * 26) - 1 \ These match dashDataOffset EQUB dashData27 - (dashData + &80 * 27) - 1 EQUB dashData28 - (dashData + &80 * 28) - 1 EQUB dashData29 - (dashData + &80 * 29) - 1 EQUB dashData30 - (dashData + &80 * 30) - 1 EQUB dashData31 - (dashData + &80 * 31) - 1 EQUB dashData32 - (dashData + &80 * 32) - 1 EQUB dashData33 - (dashData + &80 * 33) - 1 EQUB dashData34 - (dashData + &80 * 34) - 1 EQUB dashData34 - (dashData + &80 * 34) - 1 \ dashData34 repeated, as block \ 35 is smaller than block 34 EQUB dashData35 - (dashData + &80 * 35) - 1 \ These are out by one, so the EQUB dashData36 - (dashData + &80 * 36) - 1 \ X-th entry is the offset for \ block X - 1 \ dashData37 skipped as block 38 \ is larger than block 37 EQUB dashData38 - (dashData + &80 * 38) - 1 \ These match dashDataOffset EQUB dashData39 - (dashData + &80 * 39) - 1 EQUB dashData40 - (dashData + &80 * 40) - 1 \ Not used for the screen buffer EQUB &20, &20 \ These bytes appear to be unused EQUB &20, &42 EQUB &65, &68 EQUB &69 \ ****************************************************************************** \ \ Name: token7 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 7 \ Deep dive: Text tokens \ \ ****************************************************************************** .token7 EQUS "Novice" \ Print "Novice" EQUB 255 \ End token EQUB &81, &81 \ These bytes appear to be unused EQUB &81, &81 EQUB &81 \ ****************************************************************************** \ \ Name: dashData31 \ Type: Variable \ Category: Screen buffer \ Summary: Contains part of the dashboard image that gets moved into screen \ memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData31 SKIP 68 EQUB &88, &EA \ These bytes appear to be unused EQUB &EA, &C8 EQUB &EA, &88 EQUB &C8, &EA EQUB &C8, &EA EQUB &EA, &88 EQUB &EA, &C8 EQUB &88, &EA \ ****************************************************************************** \ \ Name: yLookupLo \ Type: Variable \ Category: Drawing pixels \ Summary: Lookup table for converting pixel y-coordinate to low byte of \ screen address \ \ ------------------------------------------------------------------------------ \ \ For character rows 0 to 7 and 16 to 31, this table returns the low byte of \ the screen address of the start of the row, for the custom screen mode. \ \ For character rows 8 to 15, the table is reused, as these locations would \ point to the blue sky, and we don't draw in the sky as it contains working \ game code. Instead, the lookup table at yLookupLo+8 contains pixel masks for \ use in the line-drawing routine at DrawDashboardLine. \ \ ****************************************************************************** .yLookupLo FOR I%, 0, 7 EQUB LO(&5800 + (I% * &140)) NEXT EQUB %01110111 \ Clear the first pixel of a mode 5 pixel byte EQUB %10111011 \ Clear the second pixel of a mode 5 pixel byte EQUB %11011101 \ Clear the third pixel of a mode 5 pixel byte EQUB %11101110 \ Clear the fourth pixel of a mode 5 pixel byte EQUB %01110111 \ Clear the first pixel of a mode 5 pixel byte EQUB %10111011 \ Clear the second pixel of a mode 5 pixel byte EQUB %11011101 \ Clear the third pixel of a mode 5 pixel byte EQUB %11101110 \ Clear the fourth pixel of a mode 5 pixel byte FOR I%, 16, 31 EQUB LO(&5800 + (I% * &140)) NEXT EQUB &81, &81 \ These bytes appear to be unused EQUB &81, &81 EQUB &81, &81 EQUB &81 \ ****************************************************************************** \ \ Name: dashData32 \ Type: Variable \ Category: Screen buffer \ Summary: Contains part of the dashboard image that gets moved into screen \ memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData32 SKIP 73 \ ****************************************************************************** \ \ Name: driverNames1 \ Type: Variable \ Category: Text \ Summary: The first batch of driver names (1 of 5) \ \ ****************************************************************************** .driverNames1 EQUS "Max Throttle" EQUS "Johnny Turbo" EQUS "Davey Rocket" EQUS "Gloria Slap " EQUB &81, &81 \ These bytes appear to be unused EQUB &81 \ ****************************************************************************** \ \ Name: dashData33 \ Type: Variable \ Category: Screen buffer \ Summary: Contains part of the dashboard image that gets moved into screen \ memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData33 SKIP 77 \ ****************************************************************************** \ \ Name: jumpShallowLeft \ Type: Variable \ Category: Drawing the track \ Summary: Branch labels for self-modifying code in the DrawShallowToLeft \ routine \ Deep dive: Drawing the track verges \ \ ****************************************************************************** .jumpShallowLeft EQUB shrl19 - shrl2 EQUB shrl17 - shrl2 EQUB shrl15 - shrl2 EQUB shrl13 - shrl2 EQUB shrl10 - shrl2 EQUB shrl8 - shrl2 EQUB shrl6 - shrl2 EQUB shrl4 - shrl2 EQUB shrl18 - shrl2 \ These are not used EQUB shrl16 - shrl2 EQUB shrl14 - shrl2 EQUB shrl12 - shrl2 EQUB shrl9 - shrl2 EQUB shrl7 - shrl2 EQUB shrl5 - shrl2 EQUB shrl3 - shrl2 \ ****************************************************************************** \ \ Name: token33 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 33 \ \ ------------------------------------------------------------------------------ \ \ The configurable values below are set in the PrintHeader routine. \ Deep dive: Text tokens \ \ ****************************************************************************** .token33 EQUB 12 \ Clear text area (clear screen) EQUB 31, 4, 3 \ Move text cursor (configurable, default is column 4, \ row 3) EQUB 200 + 34 \ Print token 34 (double-height, text and colours are \ configurable) EQUB 160 + 10 \ Print spaces (configurable, default is 10 spaces) EQUB 200 + 34 \ Print token 34 (double-height, text and colours are \ configurable) EQUB 31, 36, 2 \ Move text cursor to column 36, row 2 EQUB 255 \ End token \ ****************************************************************************** \ \ Name: ResetBestLapTime \ Type: Subroutine \ Category: Drivers \ Summary: Reset the best lap time to 10:00.0 for a specific driver \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The driver number (0 to 19) \ \ ****************************************************************************** .ResetBestLapTime LDA #0 \ Zero the bestLapTenths entry for driver X STA bestLapTenths,X STA bestLapSeconds,X \ Zero the bestLapSeconds entry for driver X LDA #&10 \ Set the bestLapMinutes for driver X to 10 (as is it a STA bestLapMinutes,X \ BCD number) RTS \ Return from the subroutine EQUB &77 \ This byte appears to be unused \ ****************************************************************************** \ \ Name: endMirror \ Type: Variable \ Category: Dashboard \ Summary: The offset from mirrorAddress for the end of each mirror segment \ \ ****************************************************************************** .endMirror EQUB &C2 \ Mirror segment 0 (left mirror, outer segment) EQUB &C0 \ Mirror segment 1 (left mirror, middle segment) EQUB &BC \ Mirror segment 2 (left mirror, inner segment) EQUB &BC \ Mirror segment 3 (right mirror, inner segment) EQUB &C0 \ Mirror segment 4 (right mirror, middle segment) EQUB &C2 \ Mirror segment 5 (right mirror, outer segment) EQUB &81, &81 \ These bytes appear to be unused EQUB &81 \ ****************************************************************************** \ \ Name: dashData34 \ Type: Variable \ Category: Screen buffer \ Summary: Contains part of the dashboard image that gets moved into screen \ memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData34 SKIP 77 \ ****************************************************************************** \ \ Name: driverNames2 \ Type: Variable \ Category: Text \ Summary: The second batch of driver names (2 of 5) \ \ ****************************************************************************** .driverNames2 EQUS "Hugh Jengine" EQUS "Desmond Dash" EQUS "Percy Veer " EQUS "Gary Clipper" EQUB &81, &81 \ These bytes appear to be unused EQUB &81, &81 \ ****************************************************************************** \ \ Name: dashData35 \ Type: Variable \ Category: Screen buffer \ Summary: Contains part of the dashboard image that gets moved into screen \ memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData35 SKIP 76 \ ****************************************************************************** \ \ Name: PrintHeader \ Type: Subroutine \ Category: Text \ Summary: Configure and print a double-height header in screen mode 7 \ Deep dive: Text tokens \ \ ------------------------------------------------------------------------------ \ \ Prints a token as a double-height header, with the position and colours given \ in the header tables, and a specific number of spaces between the top and \ bottom parts of the double-height text (to ensure they line up). \ \ The tokens are formatted as follows: \ \ * Token 0 ("FORMULA 3 CHAMPIONSHIP") \ Column 4, row 3 \ Yellow on red \ 10 spaces \ \ * Token 1 (" POINTS ") \ Column 7, row 0 \ Yellow on red \ 15 spaces \ \ * Token 2 ("GRID POSITIONS") \ Column 9, row 0 \ White on magenta \ 19 spaces \ \ * Token 3 ("ACCUMULATED POINTS") \ Column 7, row 0 \ White on blue \ 15 spaces \ \ * Token 4 ("REVS REVS REVS") \ Column 0, row 4 \ The colours of each letter in REVS are magenta/yellow/cyan/green \ 2 spaces \ \ This token actually prints characters 141, 163, 157, 127 before printing \ token 4 (which it does twice, one for each part of the header). 127 is \ the DEL character, so this is the same as printing just 141 and 163, \ which sets double-height, and then shows graphics character 163. This \ latter character will blank as we are still in alphanumeric text mode... \ so overall this just displays a double-height token 4, as token 4 contains \ all the colour information for the individual letters \ \ * Token 5 ("THE PITS") \ Column 11, row 4 \ Blue on yellow \ 24 spaces \ \ * Token 6 (" BEST LAP TIMES ") \ Column 7, row 0 \ White on magenta \ 15 spaces \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The number of the token to print as a double-height \ header \ \ ****************************************************************************** .PrintHeader LDA xHeader,X \ Set the x-coordinate for the text in token 33 STA token33+2 LDA yHeader,X \ Set the y-coordinate for the text in token 33 STA token33+3 LDA headerSpaces,X \ Set the number of spaces in token 33 STA token33+5 LDA headerBackground,X \ Set the background colour in token 34 STA token34+1 LDA headerForeground,X \ Set the foreground colour in token 34 STA token34+3 TXA \ Set the token embedded in token 34 to token X CLC ADC #200 STA token34+4 LDX #33 \ Print token 33, which prints token 34 in double-height JSR PrintToken \ text with the colours and position configured above RTS \ Return from the subroutine EQUB &79, &7B \ These bytes appear to be unused EQUB &7C, &7D EQUB &7E \ ****************************************************************************** \ \ Name: token1 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 1 \ Deep dive: Text tokens \ \ ****************************************************************************** .token1 EQUB 160 + 5 \ Print 5 spaces EQUB 200 + 51 \ Print token 51 (" POINTS") EQUB 160 + 6 \ Print 6 spaces EQUB 255 \ End token EQUB &81, &81 \ These bytes appear to be unused EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 \ ****************************************************************************** \ \ Name: dashData36 \ Type: Variable \ Category: Screen buffer \ Summary: Contains part of the dashboard image that gets moved into screen \ memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData36 SKIP 58 \ ****************************************************************************** \ \ Name: driverNames3 \ Type: Variable \ Category: Text \ Summary: The third batch of driver names (3 of 5) \ \ ****************************************************************************** .driverNames3 EQUS "Willy Swerve" EQUS "Sid Spoiler " EQUS "Billy Bumper" EQUS "Slim Chance " \ ****************************************************************************** \ \ Name: token40 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 40 \ Deep dive: Text tokens \ \ ****************************************************************************** .token40 EQUS "Lap Time" \ Print "Lap Time" EQUB 160 + 3 \ Print 3 spaces EQUS ":" \ Print ":" EQUB 160 + 9 \ Print 9 spaces EQUS "Best Time" \ Print "Best Time" EQUB 160 + 8 \ Print 8 spaces EQUB 255 \ End token EQUB &81, &81 \ These bytes appear to be unused EQUB &81, &81 EQUB &81, &81 \ ****************************************************************************** \ \ Name: dashData37 \ Type: Variable \ Category: Screen buffer \ Summary: Contains part of the dashboard image that gets moved into screen \ memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData37 SKIP 52 \ ****************************************************************************** \ \ Name: PrintGearNumber \ Type: Subroutine \ Category: Text \ Summary: Print the number of the current gear in double-width characters on \ the gear stick \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The gear number to print on the stick: \ \ * 0 = reverse \ \ * 1 = neutral \ \ * 2-7 = 1 to 5 \ \ ****************************************************************************** .PrintGearNumber LDA #34 \ Move the cursor to character column 34 STA xCursor STA W \ Set W to a non-zero value with bit 7 clear, so the \ call to PrintCharacter-6 prints the left half of the \ double-width character LDA #215 \ Move the cursor to pixel row 215 STA yCursor LDX gearNumber \ Set X to the current gear number LDA gearNumberText,X \ Set A to the character to print for this gear number JSR PrintCharacter-6 \ Print the left half of the double-width character LDX #&FF \ Set W to a non-zero value with bit 7 set, so the STX W \ call to PrintCharacter-6 prints the right half of the \ double-width character JSR PrintCharacter-6 \ Print the right half of the double-width character RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: ResetBestLapTimes \ Type: Subroutine \ Category: Drivers \ Summary: Reset the best lap times to 10:00.0 for all drivers \ \ ****************************************************************************** .ResetBestLapTimes LDX #19 \ We are about to reset the current lap times for \ all 20 drivers, so set a driver counter in X .rall1 JSR ResetBestLapTime \ Reset the best lap time to 10:00.0 for driver X DEX \ Decrement the driver counter BPL rall1 \ Loop back to reset the next set of bytes until we have \ reset all 20 drivers RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: token52 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 52 \ Deep dive: Text tokens \ \ ****************************************************************************** .token52 EQUB 133 \ Set foreground colour to magenta alphanumeric EQUS "R" \ Print "R" EQUB 131 \ Set foreground colour to yellow alphanumeric EQUS "E" \ Print "E" EQUB 134 \ Set foreground colour to cyan alphanumeric EQUS "V" \ Print "V" EQUB 130 \ Set foreground colour to green alphanumeric EQUS "S" \ Print "S" EQUB 255 \ End token EQUB &75, &75 \ These bytes appear to be unused \ ****************************************************************************** \ \ Name: token28 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 28 \ Deep dive: Text tokens \ \ ****************************************************************************** .token28 EQUB 200 + 54 \ Print token 54 ("FORMULA 3 CHAMPIONSHIP" header) EQUB 200 + 35 \ Print token 35 (cyan, move cursor to prompt position) EQUB 160 + 6 \ Print 6 spaces EQUB 200 + 10 \ Print token 10 ("SELECT ") EQUS "NUMBER OF LAPS" \ Print "NUMBER OF LAPS" EQUB 200 + 36 \ Print token 36 (menu option 1 with "PRESS" prompt) EQUB 200 + 18 \ Print token 18 (" 5") EQUB 200 + 14 \ Print token 14 (" laps") EQUB 200 + 37 \ Print token 37 (menu option 2) EQUB 200 + 19 \ Print token 19 ("10") EQUB 200 + 14 \ Print token 14 (" laps") EQUB 200 + 38 \ Print token 38 (menu option 3) EQUB 200 + 20 \ Print token 20 ("20") EQUB 200 + 14 \ Print token 14 (" laps") EQUB 255 \ End token \ ****************************************************************************** \ \ Name: dashData38 \ Type: Variable \ Category: Screen buffer \ Summary: Contains part of the dashboard image that gets moved into screen \ memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData38 SKIP 52 \ ****************************************************************************** \ \ Name: driverNames4 \ Type: Variable \ Category: Text \ Summary: The fourth batch of driver names (4 of 5) \ \ ****************************************************************************** .driverNames4 EQUS "Harry Fume " EQUS "Dan Dipstick" EQUS "Wilma Cargo " EQUS "Miles Behind" \ ****************************************************************************** \ \ Name: token5 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 5 \ Deep dive: Text tokens \ \ ****************************************************************************** .token5 EQUS "THE PITS" \ Print "THE PITS" EQUB 255 \ End token \ ****************************************************************************** \ \ Name: token36 \ Type: Variable \ Category: Text \ Summary: Text for recursive token 36 \ Deep dive: Text tokens \ \ ****************************************************************************** .token36 EQUB 31, 4, 14 \ Move text cursor to column 4, row 14 EQUB 136 \ Set flashing text EQUB 134 \ Set foreground colour to cyan alphanumeric EQUB 200 + 17 \ Print token 17 ("PRESS ") EQUB 31, 5, 16 \ Move text cursor to column 5, row 16 EQUB 132, 157 \ Set background colour to blue EQUB 134 \ Set foreground colour to cyan alphanumeric EQUS "1" \ Print "1" EQUB 160 + 2 \ Print 2 spaces EQUB 156 \ Set background colour to black EQUB 160 + 5 \ Print 5 spaces EQUB 131 \ Set foreground colour to yellow alphanumeric EQUB 255 \ End token \ ****************************************************************************** \ \ Name: dashData39 \ Type: Variable \ Category: Screen buffer \ Summary: Contains part of the dashboard image that gets moved into screen \ memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData39 SKIP 52 \ ****************************************************************************** \ \ Name: Print234DigitBCD \ Type: Subroutine \ Category: Text \ Summary: Print a specific driver's accumulated points as a padded two-, \ three- or four-digit number \ \ ------------------------------------------------------------------------------ \ \ Print (totalPointsHi totalPointsLo) for driver X as a four-digit number, \ followed by a space. The first two digits are printed as spaces if the high \ byte is zero, and the third digit is printed as a space if applicable. \ \ ****************************************************************************** .Print234DigitBCD LDA #2 \ Print two spaces JSR PrintSpaces LDA #%00100000 \ Set G = %00100000, so if we print the high byte and STA G \ the first digit is 0, it will be replaced by a space LDA totalPointsHi,X \ Set A to the X-th totalPointsHi value BNE Print4DigitBCD \ If A is non-zero, jump to Print4DigitBCD to print the \ (totalPointsHi totalPointsLo) for driver X as a \ four-digit number LDA #2 \ Otherwise print two spaces for the first two digits, JSR PrintSpaces \ as the high byte is zero LSR G \ Shift G right one place to give to %00010000, so the \ next call to Print2DigitBCD will print a space for the \ first digit if it is zero BNE Print4DigitBCD+3 \ Jump to Print4DigitBCD+3 to print the second two \ digits in totalPointsLo (this BNE is effectively a JMP \ as the result of the LSR is never zero) \ ****************************************************************************** \ \ Name: Print4DigitBCD \ Type: Subroutine \ Category: Text \ Summary: Print a specific driver's accumulated points as a four-digit \ number \ \ ------------------------------------------------------------------------------ \ \ Print (totalPointsHi totalPointsLo) for driver X as a 4-digit number, followed \ by a space. The second digit is always printed. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ A Always called with totalPointsHi,X \ \ ------------------------------------------------------------------------------ \ \ Other entry points: \ \ Print4DigitBCD+3 Do not print the first two digits (i.e. omit printing A) \ \ ****************************************************************************** .Print4DigitBCD JSR Print2DigitBCD \ Print the binary coded decimal (BCD) number in A LDA totalPointsLo,X \ Print the low byte of the total accumulated points for JSR Print2DigitBCD \ driver X, which is a binary coded decimal (BCD) number LDA #1 \ Print a space JSR PrintSpaces RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: FlushSoundBuffers \ Type: Subroutine \ Category: Sound \ Summary: Flush all four specified sound buffers \ Deep dive: The engine sounds \ \ ****************************************************************************** .FlushSoundBuffers LDX #3 \ We are about to flush all four sound channel buffers \ (0 to 3), so set a loop counter in X .flub1 JSR FlushSoundBuffer \ Flush the buffer for sound channel X DEX \ Decrement the loop counter BPL flub1 \ Loop back until we have flushed all four buffers RTS \ Return from the subroutine EQUB &00 \ This byte appears to be unused \ ****************************************************************************** \ \ Name: dashRightEdge \ Type: Variable \ Category: Dashboard \ Summary: Storage for the first track pixel byte along the right edge of the \ dashboard \ \ ------------------------------------------------------------------------------ \ \ This table is used to store the track pixel byte that would be shown along \ the right edge of the dashboard, but which is partially obscured by the edge. \ This is stored so we can retrieve it when masking the pixel byte with the \ dashboard edge when we draw the track line that starts at the right edge of \ the dashboard. \ \ There is a byte for each track line from 43 (the track line at the top of the \ dashboard) down to 3 (the lowest track line, just above where the wing mirror \ joins the car body). Lines 0 to 2 are not used. \ \ ****************************************************************************** .dashRightEdge EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 EQUB &81, &81 \ ****************************************************************************** \ \ Name: dashData40 \ Type: Variable \ Category: Screen buffer \ Summary: Contains part of the dashboard image that gets moved into screen \ memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** .dashData40 SKIP 52 \ ****************************************************************************** \ \ Name: driverNames5 \ Type: Variable \ Category: Text \ Summary: The fifth batch of driver names (5 of 5) \ \ ------------------------------------------------------------------------------ \ \ The last driver name in this batch is used to store the player's name. \ \ ****************************************************************************** .driverNames5 EQUS "Roland Slide" EQUS "Rick Shaw " EQUS "Peter Out " EQUS "Dummy Driver" \ ****************************************************************************** \ \ Name: objectScaffold \ Type: Variable \ Category: 3D objects \ Summary: The scaffold used to construct each object, in a scalable format \ Deep dive: Scaling objects with scaffolds \ \ ------------------------------------------------------------------------------ \ \ This table contains an object's scaffold, in a format that supports quick and \ easy scaling (see the ScaleObject routine). \ \ Each object has its own scaffold, which contains all the measurements that we \ need to build that object. The object is constructed using only measurements \ from the scaffold, so if we want to scale the object, we can just scale the \ scaffold. \ \ Each object has a number of entries in this table, one for each scaffold \ measurement, in decreasing order of size (so the largest measurements come \ first). Each scaffold measurement is in one of these binary formats: \ \ %00000ccc \ %1abbbccc \ \ where a = %a, b = %bbb and c = %ccc. \ \ The value represented by %00000ccc is: \ \ 1 \ --------- \ 2^(c - 2) \ \ and the value represented by %1abbbccc is: \ \ a 1 1 \ - + --------- + --------- \ 2 2^(b - 2) 2^(c - 2) \ \ In both cases, the result is a multiple of 1/32, so each of these entries \ represents a fraction of the form n/32. \ \ The ScaleObject routine takes the scaffold for a specific object and scales it \ by multiplying each scaffold measurement by the following: \ \ scaleUp \ ----------- \ 2^scaleDown \ \ The resulting values are stored in the scaledScaffold table, which uses the \ same structure as the object's section in the objectScaffold table, but \ contains the scaled scaffold to use when drawing the scaled object. \ \ ****************************************************************************** .objectScaffold \ Object type 0 = 24, 22, 18, 17, 16, 8, 5, 4 EQUB %10011100 \ 1 0 011 100 a = 0 b = 3 c = 4 \ 0/2 + 1/2^1 + 1/2^2 = 24/32 EQUB %11101110 \ 1 1 101 110 a = 1 b = 5 c = 6 \ 1/2 + 1/2^3 + 1/2^4 = 22/32 EQUB %10011110 \ 1 0 011 110 a = 0 b = 3 c = 6 \ 0/2 + 1/2^1 + 1/2^4 = 18/32 EQUB %10011111 \ 1 0 011 111 a = 0 b = 3 c = 7 \ 0/2 + 1/2^1 + 1/2^5 = 17/32 EQUB %00000011 \ 0 0 000 011 c = 3 \ 1/2^1 = 16/32 EQUB %00000100 \ 0 0 000 100 c = 4 \ 1/2^2 = 8/32 EQUB %10101111 \ 1 0 101 111 a = 0 b = 5 c = 7 \ 0/2 + 1/2^3 + 1/2^5 = 5/32 EQUB %00000101 \ 0 0 000 101 c = 5 \ 1/2^3 = 4/32 \ Object type 1 = 20, 12, 9, 8, 6, 5, 2, 1 EQUB %10011101 \ 1 0 011 101 a = 0 b = 3 c = 5 \ 0/2 + 1/2^1 + 1/2^3 = 20/32 EQUB %10100101 \ 1 0 100 101 a = 0 b = 4 c = 5 \ 0/2 + 1/2^2 + 1/2^3 = 12/32 EQUB %10100111 \ 1 0 100 111 a = 0 b = 4 c = 7 \ 0/2 + 1/2^2 + 1/2^5 = 9/32 EQUB %00000100 \ 0 0 000 100 c = 4 \ 1/2^2 = 8/32 EQUB %10101110 \ 1 0 101 110 a = 0 b = 5 c = 6 \ 0/2 + 1/2^3 + 1/2^4 = 6/32 EQUB %10101111 \ 1 0 101 111 a = 0 b = 5 c = 7 \ 0/2 + 1/2^3 + 1/2^5 = 5/32 EQUB %00000110 \ 0 0 000 110 c = 6 \ 1/2^4 = 2/32 EQUB %00000111 \ 0 0 000 111 c = 7 \ 1/2^5 = 1/32 \ Object type 2 = 26, 24, 18, 17, 16, 5, 3, 2 EQUB %11100110 \ 1 1 100 110 a = 1 b = 4 c = 6 \ 1/2 + 1/2^2 + 1/2^4 = 26/32 EQUB %10011100 \ 1 0 011 100 a = 0 b = 3 c = 4 \ 0/2 + 1/2^1 + 1/2^2 = 24/32 EQUB %10011110 \ 1 0 011 110 a = 0 b = 3 c = 6 \ 0/2 + 1/2^1 + 1/2^4 = 18/32 EQUB %10011111 \ 1 0 011 111 a = 0 b = 3 c = 7 \ 0/2 + 1/2^1 + 1/2^5 = 17/32 EQUB %00000011 \ 0 0 000 011 c = 3 \ 1/2^1 = 16/32 EQUB %10101111 \ 1 0 101 111 a = 0 b = 5 c = 7 \ 0/2 + 1/2^3 + 1/2^5 = 5/32 EQUB %10110111 \ 1 0 110 111 a = 0 b = 6 c = 7 \ 0/2 + 1/2^4 + 1/2^5 = 3/32 EQUB %00000110 \ 0 0 000 110 c = 6 \ 1/2^4 = 2/32 \ Object type 3 = 16, 10, 6, 4, 3, 1 EQUB %00000011 \ 0 0 000 011 c = 3 \ 1/2^1 = 16/32 EQUB %10100110 \ 1 0 100 110 a = 0 b = 4 c = 6 \ 0/2 + 1/2^2 + 1/2^4 = 10/32 EQUB %10101110 \ 1 0 101 110 a = 0 b = 5 c = 6 \ 0/2 + 1/2^3 + 1/2^4 = 6/32 EQUB %00000101 \ 0 0 000 101 c = 5 \ 1/2^3 = 4/32 EQUB %10110111 \ 1 0 110 111 a = 0 b = 6 c = 7 \ 0/2 + 1/2^4 + 1/2^5 = 3/32 EQUB %00000111 \ 0 0 000 111 c = 7 \ 1/2^5 = 1/32 \ Object type 4 = 26, 17, 16, 12, 6, 5, 3, 1 EQUB %11100110 \ 1 1 100 110 a = 1 b = 4 c = 6 \ 1/2 + 1/2^2 + 1/2^4 = 26/32 EQUB %10011111 \ 1 0 011 111 a = 0 b = 3 c = 7 \ 0/2 + 1/2^1 + 1/2^5 = 17/32 EQUB %00000011 \ 0 0 000 011 c = 3 \ 1/2^1 = 16/32 EQUB %10100101 \ 1 0 100 101 a = 0 b = 4 c = 5 \ 0/2 + 1/2^2 + 1/2^3 = 12/32 EQUB %10101110 \ 1 0 101 110 a = 0 b = 5 c = 6 \ 0/2 + 1/2^3 + 1/2^4 = 6/32 EQUB %10101111 \ 1 0 101 111 a = 0 b = 5 c = 7 \ 0/2 + 1/2^3 + 1/2^5 = 5/32 EQUB %10110111 \ 1 0 110 111 a = 0 b = 6 c = 7 \ 0/2 + 1/2^4 + 1/2^5 = 3/32 EQUB %00000111 \ 0 0 000 111 c = 7 \ 1/2^5 = 1/32 \ Object type 5 = 26, 17, 3 EQUB %11100110 \ 1 1 100 110 a = 1 b = 4 c = 6 \ 1/2 + 1/2^2 + 1/2^4 = 26/32 EQUB %10011111 \ 1 0 011 111 a = 0 b = 3 c = 7 \ 0/2 + 1/2^1 + 1/2^5 = 17/32 EQUB %10110111 \ 1 0 110 111 a = 0 b = 6 c = 7 \ 0/2 + 1/2^4 + 1/2^5 = 3/32 \ Object type 6 = 16, 10, 1 EQUB %00000011 \ 0 0 000 011 c = 3 \ 1/2^1 = 16/32 EQUB %10100110 \ 1 0 100 110 a = 0 b = 4 c = 6 \ 0/2 + 1/2^2 + 1/2^4 = 10/32 EQUB %00000111 \ 0 0 000 111 c = 7 \ 1/2^5 = 1/32 \ Object type 7 = 28, 20, 18, 16, 8 EQUB %11100101 \ 1 1 100 101 a = 1 b = 4 c = 5 \ 1/2 + 1/2^2 + 1/2^3 = 28/32 EQUB %10011101 \ 1 0 011 101 a = 0 b = 3 c = 5 \ 0/2 + 1/2^1 + 1/2^3 = 20/32 EQUB %10011110 \ 1 0 011 110 a = 0 b = 3 c = 6 \ 0/2 + 1/2^1 + 1/2^4 = 18/32 EQUB %00000011 \ 0 0 000 011 c = 3 \ 1/2^1 = 16/32 EQUB %00000100 \ 0 0 000 100 c = 4 \ 1/2^2 = 8/32 \ Object type 8 = 18, 16, 3, 2 EQUB %10011110 \ 1 0 011 110 a = 0 b = 3 c = 6 \ 0/2 + 1/2^1 + 1/2^4 = 18/32 EQUB %00000011 \ 0 0 000 011 c = 3 \ 1/2^1 = 16/32 EQUB %10110111 \ 1 0 110 111 a = 0 b = 6 c = 7 \ 0/2 + 1/2^4 + 1/2^5 = 3/32 EQUB %00000110 \ 0 0 000 110 c = 6 \ 1/2^4 = 2/32 \ Object type 9 = 16, 12, 10, 3 EQUB %00000011 \ 0 0 000 011 c = 3 \ 1/2^1 = 16/32 EQUB %10100101 \ 1 0 100 101 a = 0 b = 4 c = 5 \ 0/2 + 1/2^2 + 1/2^3 = 12/32 EQUB %10100110 \ 1 0 100 110 a = 0 b = 4 c = 6 \ 0/2 + 1/2^2 + 1/2^4 = 10/32 EQUB %10110111 \ 1 0 110 111 a = 0 b = 6 c = 7 \ 0/2 + 1/2^4 + 1/2^5 = 3/32 \ Object type 10 = 10, 9, 6, 4, 1 EQUB %10100110 \ 1 0 100 110 a = 0 b = 4 c = 6 \ 0/2 + 1/2^2 + 1/2^4 = 10/32 EQUB %10100111 \ 1 0 100 111 a = 0 b = 4 c = 7 \ 0/2 + 1/2^2 + 1/2^5 = 9/32 EQUB %10101110 \ 1 0 101 110 a = 0 b = 5 c = 6 \ 0/2 + 1/2^3 + 1/2^4 = 6/32 EQUB %00000101 \ 0 0 000 101 c = 5 \ 1/2^3 = 4/32 EQUB %00000111 \ 0 0 000 111 c = 7 \ 1/2^5 = 1/32 \ Object type 11 = 10, 8, 6, 5 EQUB %10100110 \ 1 0 100 110 a = 0 b = 4 c = 6 \ 0/2 + 1/2^2 + 1/2^4 = 10/32 EQUB %00000100 \ 0 0 000 100 c = 4 \ 1/2^2 = 8/32 EQUB %10101110 \ 1 0 101 110 a = 0 b = 5 c = 6 \ 0/2 + 1/2^3 + 1/2^4 = 6/32 EQUB %10101111 \ 1 0 101 111 a = 0 b = 5 c = 7 \ 0/2 + 1/2^3 + 1/2^5 = 5/32 \ Object type 12 = 10, 8, 6, 5 EQUB %10100110 \ 1 0 100 110 a = 0 b = 4 c = 6 \ 0/2 + 1/2^2 + 1/2^4 = 10/32 EQUB %00000100 \ 0 0 000 100 c = 4 \ 1/2^2 = 8/32 EQUB %10101110 \ 1 0 101 110 a = 0 b = 5 c = 6 \ 0/2 + 1/2^3 + 1/2^4 = 6/32 EQUB %10101111 \ 1 0 101 111 c = 0 b = 5 c = 7 \ 0/2 + 1/2^3 + 1/2^5 = 5/32 \ ****************************************************************************** \ \ Name: GetSectionSteering \ Type: Subroutine \ Category: Tactics \ Summary: Calculate the optimum steering for each track section \ Deep dive: Tactics of the non-player drivers \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The class of race: \ \ * 0 = Novice \ \ * 1 = Amateur \ \ * 2 = Professional \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ X X is unchanged \ \ ****************************************************************************** .GetSectionSteering LDA trackBaseSpeed,X \ Set baseSpeed = the X-th byte of trackBaseSpeed STA baseSpeed \ \ so baseSpeed contains the base speed for cars at the \ chosen class or race, on this track \ \ For Silverstone, this is: \ \ * 134 for Novice \ * 146 for Amateur \ * 152 for Professional STA U \ Set U = baseSpeed LDA trackSectionCount \ Set Y = trackSectionCount >> 3 LSR A \ LSR A \ so Y contains the number of sections in this track LSR A TAY \ Now we copy Y bytes (one per track section) from \ trackSteering to sectionSteering, processing each \ byte as we go (i.e. taking the input from \ trackSteering and storing the result in \ sectionSteering): \ \ * Bit 7 of the result = bit 0 of the input \ \ * Bit 6 of the result = 0 \ \ * Bits 0-5 of the result are: \ \ * A >> 2 * U / 256 if bit 1 of the input is clear \ * A >> 2 if bit 1 of the input is set \ \ where A is the input from trackSteering and U is the \ base speed from above .slin1 LDA trackSteering,Y \ Fetch the Y-th byte from trackSteering as the input LSR A \ Shift bit 0 of the input into the C flag and store it PHP \ on the stack so we can put it into bit 7 of the result LSR A \ Shift bit 1 of the input into the C flag BCS slin2 \ If bit 1 of the input is set, skip the following \ instruction JSR Multiply8x8 \ Bit 1 of the input is clear, so set (A T) = A * U \ \ i.e. A = A * U / 256 .slin2 ASL A \ Set bit 7 of the result to bit 0 of the input PLP ROR A STA sectionSteering,Y \ Store the result in the Y-th byte of sectionSteering DEY \ Decrement the loop counter BPL slin1 \ Loop back until we have processed all Y bytes RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: ApplyElevation (Part 1 of 5) \ Type: Subroutine \ Category: Driving model \ Summary: Calculate changes in the car's elevation \ Deep dive: The core driving model \ Jumps and drops \ \ ------------------------------------------------------------------------------ \ \ Calculate the following: \ \ * liftFromTorque = the lift of the front of the car caused by accelerating \ or braking, in the range -5 to +3 \ \ A positive value of liftFromTorque means the horizon is drawn lower on the \ screen, which means the nose is being pulled up. \ \ The value of liftFromTorque is only changed if the car is on the ground. The \ logic is as follows: \ \ * Horizon goes down when we are not in reverse, we are applying the throttle \ and engine torque is non-zero (i.e. we pull the nose of the car up by \ incrementing liftFromTorque, to a maximum of 3) \ \ * Horizon goes up when we are not in reverse, we are applying the brakes and \ we are moving (i.e. we push the nose of the car down by decrementing \ liftFromTorque, to a minimum of -5) \ \ * Otherwise, gradually bring the nose back to the normal level of zero, with \ the value incrementing towards zero at twice the rate that it decrements \ towards zero (so the nose rises at twice the speed that it falls, as the \ suspension springs are more powerful than gravity) \ \ ****************************************************************************** .ApplyElevation LDA heightAboveTrack \ Set A = heightAboveTrack BEQ elev1 \ If heightAboveTrack = 0, jump to elev1 DEC yJumpHeight \ Set yJumpHeight = yJumpHeight - 2 DEC yJumpHeight JMP elev7 \ Jump to elev7 .elev1 \ If we get here then heightAboveTrack = 0 and A = 0 STA yJumpHeight \ Set yJumpHeight = 0 STA yGravityDelta \ Set yGravityDelta = 0 LDY liftFromTorque \ Set Y = liftFromTorque LDA gearNumber \ If gearNumber = 0, then we are in reverse, so jump to BEQ elev3 \ elev3 LDA throttleBrakeState \ If throttleBrakeState is negative, then there is no BMI elev3 \ brake or throttle key press, so jump to elev3 BEQ elev2 \ If throttleBrakeState = 0, then the brakes are being \ applied, so jump to elev2 \ If we get here then the throttle is being applied LDA engineTorque \ Set A to the engine torque BNE elev4 \ If the engine torque is non-zero, jump to elev4 to \ increment the lift in Y BEQ elev3 \ Jump to elev3 (this BEQ is effectively a JMP as A is \ always zero) .elev2 \ If we get here then we are not in reverse gear and the \ brakes are being applied LDA playerSpeedHi \ Set A = playerSpeedHi BNE elev5 \ If A is non-zero, then we are moving, so jump to elev5 \ to decrement the lift in Y .elev3 \ This part of the routine brings the car nose back \ towards the normal level, if it isn't there already \ \ We get here when any of the following are true: \ \ * In reverse \ \ * Not in reverse, no brake or throttle key press \ \ * Not in reverse, brakes are being applied, we are \ not moving \ \ * Not in reverse, no brakes, throttle is being \ applied, engine torque is zero \ \ In other words, we are not accelerating or braking, so \ we slowly cancel any rise or fall of the car nose that \ we may have applied previously, as follows: \ \ * If the lift in Y is positive, we decrement it \ towards zero via elev5 \ \ * If the lift in Y is negative, we increment it \ towards zero and then increment it again via elev4 \ \ This means the list increments towards zero at twice \ the rate that it decrements towards zero TYA \ Set A = Y \ = liftFromTorque BEQ elev7 \ If liftFromTorque = 0, jump to elev7 to move on to \ part 2 without updating liftFromTorque BPL elev5 \ If liftFromTorque > 0, jump to elev5 to decrement the \ lift in Y \ If we get here then liftFromTorque < 0, so the nose of \ the car is already being pushed up and the horizon is \ lower than it would normally be INY \ Set Y = Y + 1 \ = liftFromTorque + 1 \ \ So this pulls the nose of the car up, making the \ horizon fall back towards the normal level .elev4 \ This part of the routine pulls the nose of the car up \ by incrementing Y (to a maximum of 3), making the \ horizon fall \ \ We jump here if we are not in reverse, we are applying \ the throttle, and engine torque is non-zero INY \ Set Y = Y + 1 \ = liftFromTorque + 1 \ \ So this pulls the nose of the car up, making the \ horizon fall down BMI elev6 \ If Y is negative, jump to elev6 to set liftFromTorque \ to Y CPY #4 \ If Y < 4, jump to elev6 to set liftFromTorque = Y BCC elev6 LDY #3 \ Set Y = 3, so Y has a maximum value of 3 BCS elev6 \ Jump to elev6 (this BCS is effectively a JMP as we \ just passed through a BCC) .elev5 \ This part of the routine pushes the nose of the car \ down by decrementing Y (to a minimum of -5), making \ the horizon rise \ \ We jump here if we are not in reverse, we are applying \ the brakes, and we are moving DEY \ Set Y = Y - 1 \ \ So this pushes the nose of the car down, making the \ horizon rise up BPL elev6 \ If Y is positive, jump to elev6 to set liftFromTorque \ to Y CPY #&FB \ If Y >= -5, jump to elev6 to set liftFromTorque = Y BCS elev6 LDY #&FB \ Set Y = -5, so Y has a minimum value of -5 .elev6 STY liftFromTorque \ Set liftFromTorque = Y \ ****************************************************************************** \ \ Name: ApplyElevation (Part 2 of 5) \ Type: Subroutine \ Category: Driving model \ Summary: Calculate the player's heading and sideways flag \ Deep dive: The core driving model \ Jumps and drops \ \ ------------------------------------------------------------------------------ \ \ Calculate the following: \ \ * playerHeading, which is the player's yaw angle, relative to the direction \ of the track, where a heading of 0 means the player is pointing straight \ along the track \ \ * playerSideways, which contains the following information: \ \ * If playerSideways < 40, then the player's car is facing forwards or \ backwards along the track \ \ * If playerSideways >= 40, then the player's car is facing sideways, \ relative to the direction of the track \ \ ****************************************************************************** .elev7 LDX playerSegmentIndex \ Set X to the number of the player's track segment from \ the track segment buffer LDY segmentVector,X \ Fetch the segment vector number for track segment X, \ which gives us the number of the segment vector for \ the track segment containing the player LDA playerPitchAngle \ Set V = playerPitchAngle STA V LDA xTrackSegmentI,Y \ Store the sign of the segment vector's x-coordinate EOR zTrackSegmentI,Y \ multiplied by the segment vector's z-coordinate on the PHP \ stack, i.e. xTrackSegmentI * zTrackSegmentI LDA zTrackSegmentI,Y \ Set A to the segment vector's z-coordinate in \ zTrackSegmentI PHP \ Store the sign of zTrackSegmentI on the stack JSR Absolute8Bit \ Set A = |A| \ = |zTrackSegmentI| CMP #60 \ Store the comparison of |zTrackSegmentI| and 60 on the PHP \ stack BCC elev8 \ If |zTrackSegmentI| < 60, jump to elev8 LDA xTrackSegmentI,Y \ Set A to the segment vector's x-coordinate JSR Absolute8Bit \ Set A = |A| \ = |xTrackSegmentI| .elev8 STA T \ Set T = A \ = |zTrackSegmentI| if |zTrackSegmentI| < 60 \ |xTrackSegmentI| if |zTrackSegmentI| >= 60 LSR A \ Set A = (A / 2 + T) / 4 CLC \ = (A / 2 + A) / 4 ADC T \ = A * 3 / 8 LSR A LSR A PLP \ Pull the result of the comparison of |zTrackSegmentI| \ and 60 from the stack BCS elev9 \ If |zTrackSegmentI| >= 60, jump to elev9 EOR #%00111111 \ We know bits 6 and 7 of A are clear, as we just \ shifted A to the right twice, so this flips bits 0 to \ 5, so the range 0 to 63 gets flipped around to 63 to 0 .elev9 PLP \ Pull the sign of zTrackSegmentI from the stack BPL elev10 \ If zTrackSegmentI is positive, jump to elev10 EOR #%10000000 \ Flip the bit 7 of A, so the range 0 to 63 gets \ changed to -65 to -128 .elev10 PLP \ Pull the sign of xTrackSegmentI * zTrackSegmentI from \ the stack JSR Absolute8Bit \ Set the sign of A to the sign of xTrackSegmentI * \ zTrackSegmentI SEC \ Set A = A - playerYawAngleHi SBC playerYawAngleHi STA playerHeading \ Set playerHeading = A \ A is an angle that represents the player's heading, \ relative to the current segment, like this: \ \ 0 \ -32 | +32 Overhead view of car \ \ | / \ \ | / 0 = looking straight ahead \ \|/ +64 = looking sharp right \ -64 -----+----- +64 -64 = looking sharp left \ /|\ \ / | \ \ / | \ \ -96 | +96 \ 128 \ \ An angle of 0 means our car is facing forwards along \ the track, while an angle of +32 means we are facing \ 45 degrees to the right of straight on, and an angle \ of 128 means we are facing backwards along the track BPL elev11 \ If A is positive, jump to elev11 to skip the following EOR #&FF \ Invert A, so this effectively reflects the angle into \ the right half of the above diagram: \ \ 0 \ | 32 \ | / \ | / \ |/ \ +----- 64 \ |\ \ | \ \ | \ \ | 96 \ 127 .elev11 CMP #64 \ If A < 64, then the player's heading is forwards, so BCC elev12 \ jump to elev12 EOR #%01111111 \ A >= 64, so the player's heading is backwards and in \ the bottom-right quadrant, so flip bits 0-6 of A, \ changing the range of the bottom-right quadrant from \ 64 to 127 to 63 to 0, to give this: \ \ 0 \ | 32 \ | / \ | / \ |/ \ +----- 63 \ |\ \ | \ \ | \ \ | 32 \ 0 \ \ If the value in A is greater than 40, then we are \ looking sideways compared to the track direction, and \ conversely, if A is less than 40, we are looking \ forwards or backwards .elev12 STA playerSideways \ Store the value of A in playerSideways, so we can test \ whether the player is facing sideways on the track \ ****************************************************************************** \ \ Name: ApplyElevation (Part 3 of 5) \ Type: Subroutine \ Category: Driving model \ Summary: Calculate player's elevation above the track \ Deep dive: The core driving model \ Jumps and drops \ \ ------------------------------------------------------------------------------ \ \ Calculate the following: \ \ * Set A to the elevation change of the car due to the sideways angle of the \ car in the current segment \ \ * playerPitchAngle = (A + bumpyGrassHeight + liftFromTorque + yJumpHeight \ + playerPitchAngle) / 2 \ \ * spinPitchAngle = playerPitchAngle - original value of playerPitchAngle \ \ ****************************************************************************** EOR #%00111111 \ We know bits 6 and 7 of A are clear, as we know A is \ in the range 0 to 63, so this flips bits 0 to 5, so \ the range 0 to 63 gets flipped around to 63 to 0, to \ give this in A: \ \ 63 \ | 32 \ | / \ | / \ |/ \ +----- 0 \ |\ \ | \ \ | \ \ | 32 \ 63 STA T \ Store the flipped range in T LSR A \ Set A = A / 2 + T CLC \ = 1.5 * T ADC T \ \ So we now have this in A, depending on the heading of \ the player within the segment: \ \ 94 \ | 48 \ | / \ | / \ |/ \ +----- 0 \ |\ \ | \ \ | \ \ | 48 \ 94 JSR MultiplyHeight \ Set: \ \ A = A * yTrackSegmentI \ \ flipping the sign if we are facing backwards \ \ The value given in yTrackSegmentI is the y-coordinate \ of the segment vector, i.e. the vector from this \ segment to the next, which is the same as the change \ in height as we move through the segment \ \ The calculation above effectively works out the \ difference in elevation of the car within the segment \ with respect to its heading, and puts it in A CLC \ Set A = A + bumpyGrassHeight + liftFromTorque ADC bumpyGrassHeight \ + yJumpHeight + playerPitchAngle CLC ADC liftFromTorque CLC ADC yJumpHeight CLC ADC playerPitchAngle CLC \ Clear the C flag, to use when A is positive BPL elev13 \ If A is positive, jump to elev13 SEC \ Set to C flag, so it contains the correct sign bit \ for the value of A .elev13 ROR A \ Set A = A / 2, keeping the sign of A intact STA playerPitchAngle \ Set playerPitchAngle = A SEC \ Set spinPitchAngle = A - V SBC V \ = playerPitchAngle - V STA spinPitchAngle \ \ We set V to the original value of playerPitchAngle \ above, so spinPitchAngle now contains the change in \ elevation of the car \ ****************************************************************************** \ \ Name: ApplyElevation (Part 4 of 5) \ Type: Subroutine \ Category: Driving model \ Summary: Calculate the height of the car above the track \ Deep dive: The core driving model \ Jumps and drops \ \ ------------------------------------------------------------------------------ \ \ Calculate the following: \ \ * yGravityDelta = max(yGravityDelta - 4, -56) \ \ * If the car falls past ground level, calculate the following to make the \ car bounce upwards by half the rate that it's falling: \ \ * yGravityDelta = |yGravityDelta| / 2 \ \ * yJumpHeight = |yGravityDelta| / 4 \ \ * spinYawAngleHi = spinYawAngleHi >> 1 with bit 7 set \ \ * heightAboveTrack = 1 \ \ and make the crash/contact sound, otherwise set: \ \ heightAboveTrack = heightAboveTrack + yGravityDelta \ \ and clip the result to the range 0 to 127 \ \ ****************************************************************************** LDA #0 \ Set W = 0, to use as the high byte of (W A) below STA W LDA yGravityDelta \ Set A = yGravityDelta - 4 SEC \ SBC #4 \ This applies the effect of gravity on the car, which \ makes the car fall faster all the time that it's \ falling BVC elev14 \ The overflow flag is set if yGravityDelta is negative LDA #&C8 \ but the result is positive, in which case we set \ A = -56, so this sets a minimum value for A of -56: \ \ A = max(yGravityDelta - 4, -56) .elev14 STA yGravityDelta \ Set yGravityDelta = A \ = max(yGravityDelta - 4, -56) CLC \ Set A = A + heightAboveTrack ADC heightAboveTrack \ = max(yGravityDelta - 4, -56) + heightAboveTrack BEQ elev15 \ If A = 0, jump to elev15 BVS elev17 \ The overflow flag is set if: \ \ * yGravityDelta + heightAboveTrack > 127 \ and both are < 128 \ (i.e. adding two positives gives a negative) \ \ * yGravityDelta + heightAboveTrack < 128 \ and both are > 127 \ (i.e. adding two negatives gives a positive) \ \ In either case, jump to elev17 to set \ heightAboveTrack = 127 BPL elev18 \ If A is positive, jump to elev18 to set \ heightAboveTrack = A .elev15 LDA yGravityDelta \ Set A = yGravityDelta JSR Absolute8Bit \ Set A = |A| \ = |yGravityDelta| CMP #5 \ If A < 5, jump to elev16 to set heightAboveTrack = 0 BCC elev16 \ If we get here then the car has fallen past ground \ level, so we need to bounce it upwards JSR ApplyBounce \ Calculate the following to make the car bounce upwards \ by half the rate that it's falling: \ \ yGravityDelta = |yGravityDelta| / 2 \ \ yJumpHeight = |yGravityDelta| / 4 \ \ heightAboveTrack = heightAboveTrack + 1 \ \ spinYawAngleHi = spinYawAngleHi >> 1 with bit 7 set \ \ and make the crash/contact sound LDA #1 \ Set A = 1, to use as the value of heightAboveTrack, \ overriding the calculation we just did BNE elev18 \ Jump to elev18 (this BNE is effectively a JMP as A is \ never zero) .elev16 LDA #0 \ Set A = 0, to use as the value of heightAboveTrack BEQ elev18 \ Jump to elev18 (this BEQ is effectively a JMP as A is \ always zero) .elev17 LDA #127 \ Set A = 127, to use as the value of heightAboveTrack .elev18 STA heightAboveTrack \ Store the value of A in heightAboveTrack \ ****************************************************************************** \ \ Name: ApplyElevation (Part 5 of 5) \ Type: Subroutine \ Category: Driving model \ Summary: Calculate the player's 3D y-coordinate and progress speed \ Deep dive: The core driving model \ Jumps and drops \ \ ------------------------------------------------------------------------------ \ \ Calculate the following: \ \ * (yPlayerCoordTop yPlayerCoordHi) = (ySegmentCoordIHi ySegmentCoordILo) \ + carProgress * yTrackSegmentI \ + heightAboveTrack * 4 \ + 172 \ \ * carSpeedHi for the player's car = playerSpeedHi * 2.13 \ \ ****************************************************************************** ASL A \ Set (W A) = (0 A) << 2 ROL W \ = (W A) << 2 ASL A \ = heightAboveTrack << 2 ROL W \ = heightAboveTrack * 4 STA V \ Set (W V) = (W A) \ = heightAboveTrack * 4 LDX currentPlayer \ Set X to the driver number of the current player LDA carProgress,X \ Set A to the lowest byte of the player's progress \ through the current segment JSR MultiplyHeight \ Set: \ \ A = A * yTrackSegmentI \ = carProgress * yTrackSegmentI \ \ flipping the sign if we are facing backwards \ \ The value given in yTrackSegmentI is the y-coordinate \ of the segment vector, i.e. the vector from this \ segment to the next, which is the same as the change \ in height as we move through the segment \ \ The calculation above effectively works out the \ difference in elevation of the car as it progresses \ through the segment, and puts it in A BPL elev19 \ If A is positive, jump to elev19 to skip the following \ instruction DEC W \ A is negative, so set W = W - 1 .elev19 LDY playerSegmentIndex \ Set Y to the index of the player's track segment from \ the track segment buffer CLC \ Set: ADC ySegmentCoordILo,Y \ \ A = A + ySegmentCoordILo \ = carProgress * yTrackSegmentI + ySegmentCoordILo \ \ ySegmentCoordILo is the low byte of the 3D \ y-coordinate for the player's track segment \ \ So A now contains the segment's y-coordinate, plus the \ elevation of the car due to the track's progress \ through the segment PHP \ Store the C flag on the stack, which will be set if \ the addition just overflowed CLC \ Set A = A + 172 ADC #172 PHP \ Store the C flag on the stack, which will be set if \ the addition just overflowed \ We now calculate the following for the y-coordinate of \ the player's 3D coordinates: \ \ (yPlayerCoordTop yPlayerCoordHi) \ \ = A \ + (W V) \ + (ySegmentCoordIHi 0) \ + C flag from the first addition \ + C flag from the second addition \ \ = carProgress * yTrackSegmentI + ySegmentCoordILo \ + 172 \ + heightAboveTrack * 4 \ + (ySegmentCoordIHi 0) \ + C flag from the first addition \ + C flag from the second addition \ \ = carProgress * yTrackSegmentI \ + 172 \ + heightAboveTrack * 4 \ + (ySegmentCoordIHi ySegmentCoordILo) \ \ with all the correct carry bits included CLC \ We start with the low bytes ADC V STA yPlayerCoordHi LDA ySegmentCoordIHi,Y \ And then the high bytes ADC W PLP ADC #0 PLP ADC #0 STA yPlayerCoordTop LDA playerSpeedHi \ Set U = playerSpeedHi, which is the player's speed in STA U \ mph LDA #33 \ Set A = 33 JSR Multiply8x8 \ Set (A T) = A * U \ = playerSpeedHi * 33 \ \ So A = playerSpeedHi * 33 / 256 ASL U \ Set A = A + U * 2 CLC \ = playerSpeedHi * 33 / 256 + playerSpeedHi * 2 ADC U \ = playerSpeedHi * 2.13 STA carSpeedHi,X \ Set carSpeedHi for the player's car to playerSpeedHi * \ 2.13 \ \ carSpeedHi is now the speed of the car in terms of \ 1/256-ths of a segment, with 120 mph in playerSpeedHi \ corresponding to a whole segment in carSpeedHi (as \ 120 * 2.13 = 255) RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: MultiplyHeight \ Type: Subroutine \ Category: Track geometry \ Summary: Multiply the height above ground of a specified track segment by A \ \ ------------------------------------------------------------------------------ \ \ For segment vector Y, calculate: \ \ A = A * yTrackSegmentI \ \ flipping the sign if we are facing backwards. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ A The number to multiply the height by \ \ Y Index of the segment vector to multiply \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ N flag Set according to the result in A \ \ ------------------------------------------------------------------------------ \ \ Other entry points: \ \ MultiplyHeight+11 Set A = A * U while retaining the sign in A (the sign of \ A must be on the stack before calling this entry point) \ \ ****************************************************************************** .MultiplyHeight STA U \ Set U to the multiplication factor in A LDA yTrackSegmentI,Y \ Store the sign of the height * directionFacing on EOR directionFacing \ the stack PHP LDA yTrackSegmentI,Y \ Set A to the track height in the Y-th yTrackSegmentI JSR Absolute8Bit \ Set A = |A| \ = |yTrackSegmentI| JSR Multiply8x8 \ Set (A T) = A * U \ = U * |yTrackSegmentI| PLP \ Set the N flag to the sign of yTrackSegmentI * \ directionFacing, so this will be set if the height \ sign is different to bit 7 of directionFacing, clear \ if the height sign matches bit 7 of directionFacing JSR Absolute8Bit \ Give A the sign in the N flag RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: MovePlayerOnTrack \ Type: Subroutine \ Category: Car geometry \ Summary: Update the position of the player's car within the current track \ segment \ Deep dive: Placing cars on the track \ \ ------------------------------------------------------------------------------ \ \ This routine sets the racing line and progress for the player's car. \ \ ****************************************************************************** .MovePlayerOnTrack \ We start by calculating the player's heading within \ the current segment, so we know which direction the \ car is heading in, and therefore how to alter the \ car's position with the track segment LDA edgeYawAngle \ Set A = edgeYawAngle - playerHeading SEC \ SBC playerHeading \ So A contains the yaw angle of the closest segment, \ from the point of view of the player's car JSR Absolute8Bit \ Set A = |A| \ \ So the angle is now like this: \ \ 0 \ | 32 \ | / \ | / \ |/ \ +----- 64 \ |\ \ | \ \ | \ \ | 96 \ 127 CMP #64 \ If A < 64, clear the C flag, if A >= 64, set the C \ flag ROR playerPastSegment \ Store the C flag in bit 7 of playerPastSegment, so \ this will be set if the yaw angle is >= 64, which is \ 90 degrees \ \ So bit 7 will be set if the closest segment is at a \ yaw angle of more than 90 degrees from the point of \ view of the player - in other words, when the player \ has driven past the segment BPL mseg1 \ If bit 7 of playerPastSegment is clear, i.e. the C \ flag is clear, then A < 64 and the player has not gone \ past the closest segment, so jump to mseg1 \ The player has gone past the closest segment, so A is \ in the range 64 to 127, with the higher figure \ indicating that the segment is way behind us, so we \ now flip that around to the range 63 to 0, where 0 \ indicates that the segment is way behind us, and 63 \ indicates that we've just passed it \ \ We use this value later to calculate the player's \ carProgress EOR #%01111111 \ Negate A using two's complement, leaving bit 7 alone CLC \ to leave the result in the range 63 to 0, so the angle ADC #1 \ is now like this: \ \ 0 \ | 32 \ | / \ | / \ |/ \ +----- 64 \ |\ \ | \ \ | \ \ | 32 \ 0 \ \ So the value of A is zero if the player's car is \ pointing straight along the track segment, or 64 if \ the car is pointing sideways, towards the verge .mseg1 PHA \ Store the player's direction in A on the stack, which \ we retrieve below to use when calculating carProgress \ for the player's car \ We now calculate the carRacingLine value for the \ player's car, which is the left-right position within \ the track LDY #186 \ Set A = edgeDistanceLo * (A' / 256) * (186 / 256) JSR GetCarInSegment \ \ where A' is A, scaled by the ScaleCarInSegment routine LDX edgeSegmentPointer \ Set X to the index of the segment within the track \ verge buffer that is closest to the player's car CPX #40 \ If X < 40, then the segment is along the inner edge of BCC mseg2 \ the track, so jump to mseg2 to skip the following \ instruction EOR #&FF \ Set A = ~A .mseg2 LDX currentPlayer \ Set X to the driver number of the current player BIT directionFacing \ Set the sign of A to that of directionFacing, so A JSR Absolute8Bit \ gets negated when we are facing backwards \ The value in A is the updated value of carRacingLine \ for the player, but before we store it, we need to use \ it to calculate the player's drift PHA \ Store A on the stack, so we can retrieve bit below \ when storing it as the updated carRacingLine for the \ player's car \ Next, we set bit 7 of playerDrift according to the \ amount of sideways movement of the player's car, with \ a set bit 7 meaning the player's car is drifting \ sideways by more than 22 in this iteration around the \ main driving loop (where the full track width is 256) SBC carRacingLine,X \ Set A = A - the racing line for the player \ \ So A contains the distance that the car has moved in \ terms of racing line, i.e. left to right BCS mseg3 \ If the subtraction didn't underflow, jump to mseg3 to \ skip the following instruction EOR #&FF \ If we get here, then A < then player's racing line and \ the subtraction underflowed, so set A = ~A to make \ A positive, so A = |A| .mseg3 CMP #22 \ If A < 22, clear the C flag, if A >= 22, set the C \ flag IF _ACORNSOFT OR _4TRACKS ROR playerDrift \ Store the C flag in bit 7 of playerDrift, so this will \ be set if A >= 22 ELIF _SUPERIOR OR _REVSPLUS JSR SetPlayerDriftSup \ Set bit 7 of playerDrift if both A >= 22 and the \ objSectionSegmt for the player is >= 3, so the \ Superior version does not record drift in the first \ three segments of a new track section ENDIF PLA \ Set carRacingLine,X to the second value we stored on STA carRacingLine,X \ the stack above \ Finally, we calculate the carProgress value for the \ player's car, which is the progress within the segment PLA \ Set A to the first value we stored on the stack above, \ which is the player's direction, where A is zero if \ the player's car is pointing straight along the track \ segment, or 64 if the car is pointing sideways, \ towards the verge EOR #&FF \ Set A = 64 - A CLC \ ADC #65 \ This works because ~A = -A - 1, so we have: \ \ A = ~A + 65 \ = -A - 1 + 65 \ = 64 - A \ \ So A is now in the range 0 to 64, where 64 means the \ car is pointing forwards along the segment, and 0 \ means it's pointing sideways LDY #136 \ Set A = edgeDistanceLo * (A' / 256) * (136 / 256) JSR GetCarInSegment \ \ where A' is A, scaled by the ScaleCarInSegment routine ASL A \ Set A = A * 2 ASL A BIT playerPastSegment \ If bit 7 of playerPastSegment is set, jump to mseg4 BMI mseg4 EOR #&FF \ Set A = ~A .mseg4 STA carProgress,X \ Set the car's progress to A RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: GetCarInSegment \ Type: Subroutine \ Category: Car geometry \ Summary: Calculate the player car's position within the current segment \ \ ------------------------------------------------------------------------------ \ \ This routine scales the value in A as follows: \ \ edgeDistanceLo * (A' / 256) * (Y / 256) \ \ where A' is A, scaled by the ScaleCarInSegment routine as follows: \ \ When Calculate Range of result \ \ A < 26 A' = 3 * A 0 to 78 \ 26 <= A < 46 A' = 4 * A + 52 156 to 232 \ A >= 46 A' = A + 190 236 to 253 \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ A The number to be scaled \ \ Y Called with Y = 136 or 186 \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ A edgeDistanceLo * A+ * (Y / 256) \ \ ****************************************************************************** .GetCarInSegment JSR ScaleCarInSegment \ Set U = A, scaled up by ScaleCarInSegment (let's call STA U \ it A') TYA \ Set (A T) = A * U JSR Multiply8x8 \ = Y * A' STA U \ Set (U T) = (A T) \ = Y * A' LDA edgeDistanceLo \ Set (A T) = A * U JSR Multiply8x8 \ = edgeDistanceLo * U \ = edgeDistanceLo * (Y * A' / 256) \ = edgeDistanceLo * A' * (Y / 256) \ \ So A = (A T) / 256 \ = edgeDistanceLo * (A' / 256) * (Y / 256) RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: ScaleCarInSegment \ Type: Subroutine \ Category: Car geometry \ Summary: Work out how far a car is within a segment by scaling the angle \ in which the car is pointing \ \ ------------------------------------------------------------------------------ \ \ This routine takes a car's heading within a segment, as follows: \ \ 0 \ | 26 \ | / \ | / _- 46 \ |/_-´ \ +--------- 64 \ \ and scales it like this: \ \ When Calculate Range of result \ \ A < 26 A' = 3 * A 0 to 78 \ 26 <= A < 46 A' = 4 * A + 52 156 to 232 \ A >= 46 A' = A + 190 236 to 253 \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ A A is in the range 0 to 63 \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ A The scaled value of A, shown as A' above \ \ ****************************************************************************** .ScaleCarInSegment CMP #26 \ If A < 26, jump to scar2 BCC scar2 CMP #46 \ If A < 46, jump to scar1 BCC scar1 \ If we get here then A >= 46 CLC \ Set A = A + 190 ADC #190 RTS \ Return from the subroutine .scar1 \ If we get here then 26 <= A < 46 ASL A \ Set A = A << 2 + 52 ASL A \ = 4 * A + 52 CLC ADC #52 RTS \ Return from the subroutine .scar2 \ If we get here then A < 26 STA T \ Set T = A ASL A \ Set A = A << 1 + T CLC \ = A * 2 + A ADC T \ = 3 * A ASL A RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: ApplyDrivingModel \ Type: Subroutine \ Category: Driving model \ Summary: Apply the driving model to the player's car \ Deep dive: An overview of the driving model \ The core driving model \ \ ****************************************************************************** .ApplyDrivingModel LDA playerYawAngleHi \ Set (A X) = (playerYawAngleHi playerYawAngleLo) LDX playerYawAngleLo JSR GetRotationMatrix \ Calculate the rotation matrix for rotating the \ player's yaw angle into the global 3D coordinate \ system, as follows: \ \ [ cosYawAngle 0 -sinYawAngle ] \ [ 0 1 0 ] \ [ sinYawAngle 0 cosYawAngle ] JSR RotateCoordToCar \ Rotate the player's delta vector from the 3D world \ coordinate system to the frame of reference of the \ player's car, putting the result into: \ \ [ xVelocity ] \ [ - ] \ [ zVelocity ] \ \ We ignore the y-coordinate as the calculations are \ all done along the ground LDA xVelocityLo \ Set (xPrevVelocityHi xPrevVelocityLo) STA xPrevVelocityLo \ = (xVelocityHi xVelocityLo) LDA xVelocityHi STA xPrevVelocityHi LDA zVelocityLo \ Set (A T) = (zVelocityHi zVelocityLo) STA T LDA zVelocityHi JSR Absolute16Bit \ Set (A T) = |A T| \ = |zVelocity| STA playerSpeedHi \ Set (playerSpeedHi playerSpeedLo) = (A T) LDA T \ = |zVelocity| STA playerSpeedLo LDY playerSpeedHi \ Set Y to the high byte of (playerSpeedHi \ playerSpeedLo) BNE dmod1 \ If the high byte is non-zero, jump to dmod1 to skip \ the following AND #%11110000 \ A contains playerSpeedLo, so this sets Y to the high TAY \ nibble of the low byte of (playerSpeedHi \ playerSpeedLo) .dmod1 STY playerMoving \ Store Y in playerMoving, which is zero if the player \ is not moving, non-zero if they are, so this denotes \ the player as moving if (playerSpeedHi playerSpeedLo) \ is non-zero, ignoring the bottom nibble of the \ low byte JSR ApplySpinYaw \ Calculate the following: \ \ xVelocity = xVelocity - (spinYawAngle * 0.34) \ \ xSpinVelocity = spinYawAngle * 0.52 JSR ApplyGrassOrTrack \ Apply the effects of driving and braking, depending on \ the current driving surface (grass or track) JSR ApplyEngine \ Apply the effects of the engine LDX #1 \ Set X = 1, so the call to ApplyTyresAndSkids processes \ the rear tyres JSR ApplyTyresAndSkids \ Calculate the forces on the rear tyres and apply skid \ forces and sound effects where applicable LDA xPrevVelocityLo \ Set (xVelocityHi xVelocityLo) to the x-coordinate of STA xVelocityLo \ the velocity vector from the previous iteration of the LDA xPrevVelocityHi \ main loop, which we stored in (xPrevVelocityHi STA xVelocityHi \ xPrevVelocityLo) LDA xVelocityLo \ Set (xVelocityHi xVelocityLo) CLC \ += (xSpinVelocityHi xSpinVelocityLo) ADC xSpinVelocityLo \ STA xVelocityLo \ starting with the low bytes LDA xVelocityHi \ And then the high bytes ADC xSpinVelocityHi STA xVelocityHi JSR ApplySteeringSpeed \ Calculate the following in parallel: \ \ zVelocity = zVelocity + xVelocity * steering \ \ xVelocity = xVelocity - zVelocity * steering LDX #0 \ Set X = 0, so the call to ApplyTyresAndSkids processes \ the front tyres JSR ApplyTyresAndSkids \ Calculate the forces on the front tyres and apply skid \ forces and sound effects where applicable JSR ApplySteeringForce \ Calculate the following in parallel: \ \ xTyreForceNose = xTyreForceNose \ + zTyreForceNose * steering \ \ zTyreForceNose = zTyreForceNose \ - xTyreForceNose * steering JSR ScaleTyreForces \ Calculate the following: \ \ spinYawDelta = (xTyreForceNose - xTyreForceRear) \ * 0.3 \ \ xTyreForceNose = xTyreForceNose >> 2 \ \ xTyreForceRear = xTyreForceRear >> 2 \ \ zTyreForceNose = zTyreForceNose >> 2 \ \ zTyreForceRear = zTyreForceRear >> 2 \ \ xPlayerAccel = (xTyreForceRear * 1.5 \ + xTyreForceNose) * 1.6 \ \ zPlayerAccel = (zTyreForceRear * 1.5 \ + zTyreForceNose) * 1.6 \ \ zTyreForceBoth = zPlayerAccelHi LDA heightAboveTrack \ If heightAboveTrack < 2, jump to dmod3 CMP #2 BCC dmod3 \ If we get here then heightAboveTrack >= 2 LDX #2 \ We now zero the three 16-bit bytes at spinYawDelta, \ xPlayerAccel and zPlayerAccel, so set a counter in X \ for the three variables LDA #0 \ Set A = 0 to use as the zero value .dmod2 STA spinYawDeltaLo,X \ Zero the X-th byte of (spinYawDeltaHi spinYawDeltaLo) STA spinYawDeltaHi,X DEX \ Decrement the variable counter BPL dmod2 \ Loop back until we have zeroed all three 16-bit \ variables .dmod3 JSR ApplyWingBalance \ Calculate the following: \ \ xPlayerAccel = xPlayerAccel \ - scaledSpeed * xPrevVelocityHi \ \ zPlayerAccel = zPlayerAccel \ - scaledSpeed \ * (wingBalance * playerSpeedHi + 2048) \ * abs(zVelocity) JSR RotateCarToCoord \ Rotate this vector: \ \ [ xPlayerAccel ] \ [ - ] \ [ zPlayerAccel ] \ \ from the frame of reference of the player's car into \ the 3D world coordinate system, putting the result \ into: \ \ [ xAcceleration ] \ [ - ] \ [ zAcceleration ] \ \ We ignore the y-coordinate as the calculations are \ all done along the ground JSR UpdateVelocity \ Calculate the following: \ \ xPlayerSpeed = xPlayerSpeed + xAcceleration << 5 \ \ zPlayerSpeed = zPlayerSpeed + zAcceleration << 3 \ \ spinYawAngle = spinYawAngle + spinYawDelta << 3 JSR ApplyDeltas \ Calculate the following: \ \ xPlayerCoord = xPlayerCoord + xPlayerSpeed * 2 \ \ zPlayerCoord = zPlayerCoord + zPlayerSpeed * 2 \ \ playerYawAngle = playerYawAngle + spinYawAngle JSR ApplyElevation \ Calculate changes in the car's elevation RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: ApplySpinYaw \ Type: Subroutine \ Category: Driving model \ Summary: Calculate variables based on the spin yaw angle \ Deep dive: The core driving model \ \ ------------------------------------------------------------------------------ \ \ Calculate the following: \ \ xVelocity = xVelocity - (spinYawAngle * 0.34) \ \ xSpinVelocity = spinYawAngle * 0.52 \ \ ****************************************************************************** .ApplySpinYaw LDA spinYawAngleHi \ Set T = spinYawAngleHi to use as the low byte in (A T) STA T LDY #88 \ Set Y = 88 LDA spinYawAngleTop \ Set (A T) = (spinYawAngleTop spinYawAngleHi) JSR Multiply8x16Signed \ Set: \ \ (A T) = (A T) * abs(A) * Y / 256 \ = spinYawAngle * abs(spinYawAngle) * 88 / 256 \ = spinYawAngle * 88 / 256 \ = spinYawAngle * 0.34 STA U \ Set (U T) = (A T) \ = spinYawAngle * 0.34 LDA xVelocityLo \ Set (xVelocityHi xVelocityLo) SEC \ = (xVelocityHi xVelocityLo) - (U T) SBC T \ = xVelocity - (spinYawAngle * 0.34) STA xVelocityLo \ \ starting with the low bytes LDA xVelocityHi \ And then the high bytes SBC U STA xVelocityHi JSR MultiplyBy1Point5 \ Set (A T) = (U T) * 1.5 \ = spinYawAngle * 0.34 * 1.5 \ = spinYawAngle * 0.52 STA xSpinVelocityHi \ Set (xSpinVelocityHi xSpinVelocityLo) = (A T) LDA T \ = spinYawAngle * 0.52 STA xSpinVelocityLo RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: Multiply8x16Signed \ Type: Subroutine \ Category: Maths (Arithmetic) \ Summary: Multiply an 8-bit and a 16-bit number and apply a sign to the \ result \ \ ------------------------------------------------------------------------------ \ \ This routine calculates: \ \ (A T) = (A T) * abs(A) * Y / 256 \ \ 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: \ \ (A T) = (A T) * Y / 256 \ \ and the following is calculated if A is negative: \ \ (A T) = -(A T) * Y / 256 \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ (A T) A 16-bit signed number \ \ Y An unsigned number \ \ N flag Controls the sign to be applied: \ \ * N flag clear to calculate (A T) * Y / 256 \ \ * N flag set to calculate -(A T) * Y / 256 \ \ ****************************************************************************** .Multiply8x16Signed PHP \ Store the N flag on the stack JSR Absolute16Bit \ Set the sign of (A T) to that of the N flag argument STA V \ Set (V T) = (A T) STY U \ Set U = Y JSR Multiply8x16 \ Set (U T) = U * (V T) / 256 \ = Y * (A T) / 256 LDA U \ Set (A T) = (U T) \ = Y * (A T) / 256 PLP \ Retrieve the N flag from the stack JSR Absolute16Bit \ Set the sign of (A T) to that of the N flag argument RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: MultiplyBy1Point5 \ Type: Subroutine \ Category: Maths (Arithmetic) \ Summary: Multiply a 16-bit signed number by 1.5 \ \ ------------------------------------------------------------------------------ \ \ Calculate the following: \ \ (A T) = (U T) * 1.5 \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ (U T) A 16-bit signed number \ \ ****************************************************************************** .MultiplyBy1Point5 LDA U \ Set A = U, the high byte of (U T) CLC \ Clear the C flag, to use when A is positive BPL mulh1 \ If A is positive, jump to mulh1 to skip the following \ instruction SEC \ Set the C flag, to use when A is negative .mulh1 \ By this point, the C flag contains the sign bit of A \ (set if negative, clear if positive) ROR A \ Shift A to the right, inserting the C flag into bit 7 \ to retain the correct sign PHA \ Store the result on the stack, so the stack contains \ the top byte of (U T), shifted right by one place LDA T \ Set A = T, the low byte of (U T) ROR A \ Shift A to the right, so the stack and A contain the \ high and low bytes of (U T) >> 1, so if S is the value \ on top of the stack, we have: \ \ (S A) = (U T) >> 1 CLC \ Set (A T) = (S A) + (U T) ADC T \ = (U T) >> 1 + (U T) STA T \ = (U T) * 1.5 \ \ starting with the low bytes PLA \ And then the high bytes ADC U RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: ApplyTyresAndSkids \ Type: Subroutine \ Category: Driving model \ Summary: Calculate the forces on the front or rear tyres and apply skid \ forces and sound effects where applicable \ Deep dive: The core driving model \ Skidding \ \ ------------------------------------------------------------------------------ \ \ Calculate the following: \ \ * If heightAboveTrack >= 2, stop the tyres from squealing \ \ * If heightAboveTrack < 2: \ \ * Call ApplyTyreForces \ \ * If either bit 6 or 7 is set in tyreSqueal for tyre X, then: \ \ * Call ApplySkidForces and make the tyres squeal \ \ otherwise: \ \ * On every other main loop iteration, stop the tyres from squealing \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The set of tyres to process: \ \ * 0 = front tyres \ \ * 1 = rear tyres \ \ ****************************************************************************** .ApplyTyresAndSkids LDA heightAboveTrack \ If heightAboveTrack >= 2, jump to gfor1 to stop the CMP #2 \ tyres from squealing BCS gfor1 JSR ApplyTyreForces \ Calculate the tyre forces on the car LDA tyreSqueal,X \ Set A to tyreSqueal for tyre X AND #%11000000 \ If either of bit 6 or 7 is set in A, jump to gfor3 to BNE gfor3 \ make the tyres squeal LDA mainLoopCounterLo \ If bit 1 of mainLoopCounterLo is set, which it is on AND #%00000010 \ two out of every four iterations round the main loop, BNE gfor2 \ then jump to gfor2 to return from the subroutine .gfor1 LDX #3 \ Flush the buffer for sound channel 3 to stop any tyre JSR FlushSoundBuffer \ squeals we might already be making .gfor2 RTS \ Return from the subroutine .gfor3 JSR ApplySkidForces \ Calculate the tyre forces when the car is skidding, \ returning them in xTyreForceNose and zTyreForceNose or \ xTyreForceRear and zTyreForceRear LDA soundBuffer+3 \ If sound buffer 3 is currently being used, then we are BNE gfor4 \ already making the sound of the tyres squealing, so \ jump to gfor4 to return from the subroutine LDY #1 \ Make sound #3 (tyre squeal) using envelope 1 LDA #3 JSR MakeSound .gfor4 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: ApplySteeringSpeed \ Type: Subroutine \ Category: Driving model \ Summary: Apply steering to the car's speed in xVelocity and zVelocity \ Deep dive: The core driving model \ Matching the code to the driving model \ \ ------------------------------------------------------------------------------ \ \ Calculate the following in parallel: \ \ zVelocity = zVelocity + xVelocity * steering \ \ xVelocity = xVelocity - zVelocity * steering \ \ ****************************************************************************** .ApplySteeringSpeed LDX #2 \ Set X = 2, so in the call to MultiplyCoords+7 we use \ (steeringHi steeringLo) as the 16-bit sign-magnitude \ value to multiply LDY #9 \ Set Y = 9, so in the call to MultiplyCoords+7 we use \ zVelocity as the 16-bit signed number LDA #%10000000 \ Clear bit 6 and set bit 7 of H, to store the result STA H \ rather than adding, and negate the result in the call \ to MultiplyCoords+7 LDA #14 \ Set A = 14, so in the call to MultiplyCoords+7, we \ store the result in xSteeringForce JSR MultiplyCoords+7 \ Set: \ \ variableA = -variableY * variableX \ \ so: \ \ xSteeringForce = -zVelocity * steering LDX #2 \ Set X = 2, so in the call to MultiplyCoords+7 we use \ (steeringHi steeringLo) as the 16-bit sign-magnitude \ value to multiply LDY #8 \ Set Y = 8, so in the call to MultiplyCoords+7 we use \ xVelocity as the 16-bit signed number LDA #%01000000 \ Set bit 6 and clear bit 7 of H, to add the result STA H \ rather than replacing, and leave the sign of the \ result alone in the call to MultiplyCoords+7 LDA #9 \ Set A = 9, so in the call to MultiplyCoords+7, we \ store the result in zVelocity JSR MultiplyCoords+7 \ Set: \ \ variableA = variableA + variableY * variableX \ \ so: \ \ zVelocity = zVelocity + xVelocity * steering LDX #8 \ Set X = 8, so the call to AddSteeringForce adds \ xSteeringForce to xVelocity JSR AddSteeringForce \ Set xVelocity = xVelocity + xSteeringForce \ = xVelocity - zVelocity * steering RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: ApplySteeringForce \ Type: Subroutine \ Category: Driving model \ Summary: Apply steering to xTyreForceNose and zTyreForceNose \ Deep dive: The core driving model \ Matching the code to the driving model \ \ ------------------------------------------------------------------------------ \ \ Calculate the following in parallel: \ \ xTyreForceNose = xTyreForceNose + zTyreForceNose * steering \ \ zTyreForceNose = zTyreForceNose - xTyreForceNose * steering \ \ ****************************************************************************** .ApplySteeringForce LDX #2 \ Set X = 2, so in the call to MultiplyCoords we use \ (steeringHi steeringLo) as the 16-bit sign-magnitude \ value to multiply LDY #12 \ Set Y = 12, so in the call to MultiplyCoords we use \ zTyreForceNose as the 16-bit signed number LDA #%00000000 \ Clear bits 6 and 7 of H, to store the result rather STA H \ than adding, and leave the sign of the result alone in \ the call to MultiplyCoords+7 LDA #14 \ Set A = 14, so in the call to MultiplyCoords+7, we \ store the result in xSteeringForce JSR MultiplyCoords+7 \ Set: \ \ variableA = variableY * variableX \ \ so: \ \ xSteeringForce = zTyreForceNose * steering LDX #2 \ Set X = 2, so in the call to MultiplyCoords we use \ (steeringHi steeringLo) as the 16-bit sign-magnitude \ value to multiply LDY #10 \ Set Y = 10, so in the call to MultiplyCoords we use \ xTyreForceNose as the 16-bit signed number LDA #%11000000 \ Set bits 6 and 7 of H, to add the result rather than STA H \ replacing, and negate the result in the call to \ MultiplyCoords+7 LDA #12 \ Set A = 12, so in the call to MultiplyCoords+7, we \ store the result in zTyreForceNose JSR MultiplyCoords+7 \ Set: \ \ variableA = variableA - variableY * variableX \ \ so: \ \ zTyreForceNose = zTyreForceNose \ - xTyreForceNose * steering LDX #10 \ Set X = 8, so the call to AddSteeringForce adds \ xSteeringForce to xTyreForceNose JSR AddSteeringForce \ Set xTyreForceNose = xTyreForceNose + xSteeringForce \ = xTyreForceNose + zTyreForceNose * steering RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: AddSteeringForce \ Type: Subroutine \ Category: Driving model \ Summary: Add the steering force to xVelocity or xTyreForceNose \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X Called with either 8 or 10: \ \ * 8: set xVelocity += xSteeringForce \ \ * 10: set xTyreForceNose += xSteeringForce \ \ ****************************************************************************** .AddSteeringForce LDA xPlayerSpeedHi,X \ Add (xSteeringForceHi xSteeringForceLo) to the CLC \ variable at offset X from (xPlayerSpeedTop ADC xSteeringForceLo \ xPlayerSpeedHi), starting with the low bytes STA xPlayerSpeedHi,X LDA xPlayerSpeedTop,X \ And then the high bytes ADC xSteeringForceHi STA xPlayerSpeedTop,X RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: ScaleTyreForces \ Type: Subroutine \ Category: Driving model \ Summary: Scale the tyre forces and calculate the combined tyre force on the \ player \ Deep dive: The core driving model \ Matching the code to the driving model \ \ ------------------------------------------------------------------------------ \ \ Calculate the following: \ \ spinYawDelta = (xTyreForceNose - xTyreForceRear) * 0.3 \ \ xTyreForceNose = xTyreForceNose >> 2 \ \ xTyreForceRear = xTyreForceRear >> 2 \ \ zTyreForceNose = zTyreForceNose >> 2 \ \ zTyreForceRear = zTyreForceRear >> 2 \ \ xPlayerAccel = (xTyreForceRear * 1.5 + xTyreForceNose) * 1.6 \ \ zPlayerAccel = (zTyreForceRear * 1.5 + zTyreForceNose) * 1.6 \ \ zTyreForceBoth = zPlayerAccelHi \ \ ****************************************************************************** .ScaleTyreForces LDY #78 \ Set Y = 78 LDA xTyreForceNoseLo \ Set (A T) = xTyreForceNose - xTyreForceRear SEC \ SBC xTyreForceRearLo \ starting with the low bytes STA T LDA xTyreForceNoseHi \ And then the high bytes SBC xTyreForceRearHi JSR Multiply8x16Signed \ Set: \ \ (A T) = (A T) * abs(A) * Y / 256 \ = (xTyreForceNose - xTyreForceRear) * 78 / 256 \ = (xTyreForceNose - xTyreForceRear) * 0.3 STA spinYawDeltaHi \ Set spinYawDelta = (A T) LDA T \ = (xTyreForceNose - xTyreForceRear) * 0.3 STA spinYawDeltaLo \ We now perform the following shifts, making sure to \ keep the signs intact: \ \ xTyreForceNose = xTyreForceNose >> 2 \ \ xTyreForceRear = xTyreForceRear >> 2 \ \ zTyreForceNose = zTyreForceNose >> 2 \ \ zTyreForceRear = zTyreForceRear >> 2 LDY #1 \ Set Y = 1, to act as a shift counter in the outer loop \ below, so we right-shift Y + 1 times (i.e. twice) .forc1 LDX #3 \ Set X = 3, to act as a variable counter in the inner \ loop to work through zTyreForceRear, zTyreForceNose, \ xTyreForceRear and xTyreForceNose (let's call this \ variableX) .forc2 LDA xTyreForceNoseHi,X \ Set A to the high byte of variableX CLC \ Clear the C flag, to use if variableX is positive BPL forc3 \ If A is positive, jump to forc3 to keep the C flag \ clear SEC \ Otherwise set the C flag, to use if variableX is \ negative \ The C flag now contains the sign bit of A .forc3 ROR xTyreForceNoseHi,X \ Set variableX = variableX >> 1 ROR xTyreForceNoseLo,X \ \ Keeping the sign intact DEX \ Decrement the inner loop counter in X BPL forc2 \ Loop back to forc2 until we have shifted all four \ variables to the right by one place DEY \ Decrement the shift counter in Y BPL forc1 \ Loop back until we have right-shifted Y + 1 times LDX #2 \ Set X = 2, to act as a variable counter in the \ following loop, iterating through values 0 and 2, as \ X is decremented twice at the end of the loop \ \ The loop references xTyreForceRear,X and \ xTyreForceNose,X, so the loop iterates through: \ \ * zTyreForceRear and zTyreForceNose when X = 2 \ \ * xTyreForceRear and xTyreForceNose when X = 0 \ \ The comments below are for when X = 2 LDA #1 \ Set G = 1, to use as the index for storing the STA G \ following calculation .forc4 LDA xTyreForceRearLo,X \ Set (U T) = zTyreForceRear STA T LDA xTyreForceRearHi,X STA U JSR MultiplyBy1Point5 \ Set (A T) = (U T) * 1.5 \ = zTyreForceRear * 1.5 STA U \ Set (U T) = (A T) \ = zTyreForceRear * 1.5 LDA T \ Set (A T) = (U T) + zTyreForceNose CLC \ = zTyreForceRear * 1.5 + zTyreForceNose ADC xTyreForceNoseLo,X \ STA T \ starting with the low bytes LDY #205 \ Set Y = 205 LDA U \ And then the high bytes ADC xTyreForceNoseHi,X JSR Multiply8x16Signed \ Set (A T) = (A T) * abs(A) * Y / 256 \ = (zTyreForceRear * 1.5 + zTyreForceNose) \ * 205 / 256 \ = (zTyreForceRear * 1.5 + zTyreForceNose) * 0.8 ASL T \ Set (A T) = (A T) * 2 ROL A \ = (zTyreForceRear * 1.5 + zTyreForceNose) * 1.6 LDY G \ Set zPlayerAccel = (A T) STA xPlayerAccelHi,Y \ = (zTyreForceRear * 1.5 + zTyreForceNose) * 1.6 LDA T STA xPlayerAccelLo,Y DEC G \ Decrement G so the next iteration stores the result in \ xPlayerAccel DEX \ Decrement X twice so the next iteration calculates DEX \ (xTyreForceRear * 1.5 + xTyreForceNose) * 1.6 BPL forc4 \ Loop back until we have calculated both zPlayerAccel \ and xPlayerAccel LDA zPlayerAccelHi \ Set zTyreForceBoth = zPlayerAccelHi STA zTyreForceBoth RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: MultiplyCoords \ 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 \ \ ------------------------------------------------------------------------------ \ \ 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 \ Type: Subroutine \ Category: Maths (Geometry) \ Summary: Subtract from a specified coordinate variable \ \ ------------------------------------------------------------------------------ \ \ 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 \ Type: Subroutine \ Category: Maths (Geometry) \ Summary: Add to a specified coordinate variable \ \ ------------------------------------------------------------------------------ \ \ 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 \ 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 \ \ ------------------------------------------------------------------------------ \ \ 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 \ 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 \ \ ------------------------------------------------------------------------------ \ \ 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 \ Type: Subroutine \ Category: Maths (Geometry) \ Summary: Rotate a vector by a rotation matrix \ \ ------------------------------------------------------------------------------ \ \ 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 \ 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 \ \ ------------------------------------------------------------------------------ \ \ 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 \ Type: Subroutine \ Category: Driving model \ Summary: Update the player's velocity and spin yaw angle \ Deep dive: The core driving model \ \ ------------------------------------------------------------------------------ \ \ 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 \ Type: Subroutine \ Category: Driving model \ Summary: Process the key press for starting the engine \ Deep dive: Modelling the engine \ \ ****************************************************************************** .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 \ 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 \ \ ------------------------------------------------------------------------------ \ \ 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 \ 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 \ \ ------------------------------------------------------------------------------ \ \ 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 \ Type: Subroutine \ Category: Driving model \ Summary: Set the rev counter and zero the engine torque \ Deep dive: Modelling the engine \ \ ------------------------------------------------------------------------------ \ \ 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 \ Type: Subroutine \ Category: Driving model \ Summary: Zero engineTorque \ Deep dive: Modelling the engine \ \ ------------------------------------------------------------------------------ \ \ 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 \ 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 \ \ ------------------------------------------------------------------------------ \ \ 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 \ Type: Subroutine \ Category: Driving model \ Summary: Set engineTorque and soundRevTarget \ Deep dive: Modelling the engine \ \ ------------------------------------------------------------------------------ \ \ 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 \ 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 \ \ ------------------------------------------------------------------------------ \ \ 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 \ Type: Subroutine \ Category: Driving model \ Summary: Calculate the tyre forces when the car is skidding \ Deep dive: Skidding \ \ ------------------------------------------------------------------------------ \ \ 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 \ Type: Subroutine \ Category: Driving model \ Summary: Apply a maximum limit to a 16-bit number, unless the throttle is \ being applied \ \ ------------------------------------------------------------------------------ \ \ 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 \ Type: Subroutine \ Category: Driving model \ Summary: Apply a maximum limit and a sign to a 16-bit number \ \ ------------------------------------------------------------------------------ \ \ 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 \ \ ------------------------------------------------------------------------------ \ \ Other entry points: \ \ lims1 Skip applying the maximum value to variableG, so we set: \ \ variableG = (A T) * abs(H) \ \ ****************************************************************************** .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 \ Type: Subroutine \ Category: Maths (Arithmetic) \ Summary: Scale up a 16-bit value by 2^5 \ \ ------------------------------------------------------------------------------ \ \ 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 \ Type: Subroutine \ Category: Driving model \ Summary: Calculate the tyre forces due to the throttle or brakes \ Deep dive: The core driving model \ Skidding \ \ ------------------------------------------------------------------------------ \ \ 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 \ 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 \ \ ------------------------------------------------------------------------------ \ \ * 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 rear 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 \ 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 \ \ ****************************************************************************** .wingForceTrack EQUB 53 \ Front wing EQUB 53 \ Rear wing \ ****************************************************************************** \ \ Name: wingForceGrass \ 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 \ \ ****************************************************************************** .wingForceGrass EQUB 25 \ Front wing EQUB 26 \ Rear wing \ ****************************************************************************** \ \ Name: ApplyWingBalance \ Type: Subroutine \ Category: Driving model \ Summary: Apply the effect of the wing settings \ Deep dive: The core driving model \ Driving on grass \ \ ------------------------------------------------------------------------------ \ \ This routine calculates 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 \ Type: Subroutine \ Category: 3D objects \ Summary: Create an object for the road sign \ Deep dive: Road signs \ \ ****************************************************************************** .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 \ 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 \ Type: Subroutine \ Category: Maths (Geometry) \ Summary: Add a scaled vector to another vector, one axis at a time \ \ ------------------------------------------------------------------------------ \ \ 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 \ Type: Subroutine \ Category: Drivers \ Summary: Initialise all 20 drivers on the starting grid \ \ ------------------------------------------------------------------------------ \ \ 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 \ Type: Subroutine \ Category: Text \ Summary: Prints a text token on the second text line at the top of the \ driving screen \ \ ------------------------------------------------------------------------------ \ \ 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 \ Type: Subroutine \ Category: Text \ Summary: Prints a text token on the first text line at the top of the \ driving screen \ \ ------------------------------------------------------------------------------ \ \ 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 \ 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 \ Type: Subroutine \ Category: Driving model \ Summary: Apply a jump to the player's car when hitting the track verge \ \ ------------------------------------------------------------------------------ \ \ 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 \ Type: Subroutine \ Category: Driving model \ Summary: Apply a bounce to the player's car when it hits the ground \ \ ------------------------------------------------------------------------------ \ \ 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 \ Type: Subroutine \ Category: Screen mode \ Summary: Switch to the custom screen mode \ Deep dive: Hidden secrets of the custom screen mode \ \ ------------------------------------------------------------------------------ \ \ 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 \ Type: Subroutine \ Category: Screen mode \ Summary: The IRQ handler for the custom screen mode \ Deep dive: Hidden secrets of the custom screen mode \ \ ------------------------------------------------------------------------------ \ \ 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 \ Type: Variable \ Category: Screen mode \ Summary: The 6845 registers for the custom screen mode \ Deep dive: Hidden secrets of the custom screen mode \ \ ------------------------------------------------------------------------------ \ \ 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 \ Type: Variable \ Category: Screen mode \ Summary: Stores the previous value of IRQ1V before we install our custom \ IRQ handler \ \ ****************************************************************************** .irq1Address EQUW 0 \ ****************************************************************************** \ \ Name: screenTimer1 \ 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 \ \ ****************************************************************************** .screenTimer1 EQUW &04D8 \ ****************************************************************************** \ \ Name: screenTimer2 \ 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 \ \ ****************************************************************************** .screenTimer2 EQUW &1064 \ ****************************************************************************** \ \ Name: KillCustomScreen \ 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 \ \ ****************************************************************************** .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 need it) CLI \ Re-enable interrupts JSR FlushSoundBuffers \ Flush all four sound channel buffers \ Fall through into SetScreenMode7 to switch to mode 7 \ ****************************************************************************** \ \ Name: SetScreenMode7 \ Type: Subroutine \ Category: Screen mode \ Summary: Change to screen mode 7 and hide the cursor \ \ ****************************************************************************** .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 \ 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 \ \ ****************************************************************************** .screenSection EQUB 0 \ ****************************************************************************** \ \ Name: MoveHorizon \ 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 \ \ ****************************************************************************** .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 \ 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 \ Type: Subroutine \ Category: Drivers \ Summary: Increment the lap number and lap times for a specific driver \ \ ------------------------------------------------------------------------------ \ \ 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 \ Type: Subroutine \ Category: Drivers \ Summary: Zero the specified timer \ \ ------------------------------------------------------------------------------ \ \ 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 \ Type: Subroutine \ Category: Text \ Summary: Print the best lap time and the current lap time at the top of the \ screen \ \ ****************************************************************************** .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 \ Type: Subroutine \ Category: Text \ Summary: Print the current lap time at the top of the screen \ \ ------------------------------------------------------------------------------ \ \ 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 \ Type: Subroutine \ Category: Keyboard \ Summary: Read the value of an ADC channel (used to read the joystick) \ \ ------------------------------------------------------------------------------ \ \ 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 \ \ * Channel 1 is the x-axis: 0 = right, 65520 = left \ \ * Channel 2 is the y-axis: 0 = down, 65520 = up 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 \ Type: Subroutine \ Category: Main loop \ Summary: Increment the timers and the main loop counter \ Deep dive: Scheduling tasks in the main loop \ \ ****************************************************************************** .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 \ 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 \ \ ****************************************************************************** .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 \ 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 \ \ ****************************************************************************** .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 \ Type: Subroutine \ Category: Text \ Summary: Print a character on-screen \ \ ------------------------------------------------------------------------------ \ \ 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 \ Type: Subroutine \ Category: Drawing pixels \ Summary: Return the screen address for a specified screen coordinate \ \ ------------------------------------------------------------------------------ \ \ 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 \ Type: Subroutine \ Category: Dashboard \ Summary: Erase a line by replacing each pixel in the line with its original \ contents \ \ ****************************************************************************** .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 \ Type: Subroutine \ Category: Dashboard \ Summary: Update the rev counter and steering wheel lines on the dashboard \ \ ****************************************************************************** .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 along 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 along 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 \ Type: Subroutine \ Category: Dashboard \ Summary: Draw the hand on the rev counter \ \ ****************************************************************************** .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 \ Type: Subroutine \ Category: Dashboard \ Summary: Draw a hand on the rev counter or a line on the steering wheel \ \ ------------------------------------------------------------------------------ \ \ 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 \ Type: Subroutine \ Category: Dashboard \ Summary: Update screen memory to animate the tyres \ \ ****************************************************************************** .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 \ We now flip the black-and-white pixels in the tyre \ tread at the top of the left and white tyres using an \ EOR with %10, which flips pixels between %00 (black) \ and %10 (white) \ \ The tyre tread spills across three character blocks, \ with each pixel byte covering four pixels. Taking the \ left tyre, we have the following blocks: \ \ 1. Top-left block: tyreLeft1 to tyreLeft1+2 \ \ Three rows of four pixels at the top-left of the \ tyre \ \ 2. Top-right block: tyreLeft2+3, tyreLeft2+4 \ \ Two rows of two pixels at the top-right of the \ tyre \ \ 3. Bottom-left block: tyreLeft3 to tyreLeft3+4 \ \ Five rows of pixels at the bottom-left of the \ tyre, covering 4, 4, 2, 2, 1 pixels as we work \ our way down \ \ This looks like this, where the number is the block \ number above (the left side of the following is along \ the left edge of the screen): \ \ 111122 \ 111122 \ 1111 \ 3333 \ 3333 \ 33 \ 33 \ 3 \ \ The code below works through the various blocks, \ applying an EOR to the tyre pixels as follows: \ \ 1. tyreLeft1, tyreLeft1+2 \ \ EOR with mask %11110000 (all four pixels) \ \ 2. tyreLeft2+3, tyreLeft2+4 \ \ EOR with mask %11000000 (left two pixels) \ \ 3. tyreLeft3 to tyreLeft3+4 \ \ EOR with mask from tyreTreadLeft table (a wedge \ shape from all four pixels at the top down to \ just one pixel at the bottom) \ \ A similar process is applied to the right tyre, but \ with the shape reflected, like this: \ \ 221111 \ 221111 \ 1111 \ 3333 \ 3333 \ 33 \ 33 \ 3 \ \ The right tyre uses tyreRight and tyreTreadRight to \ achieve the same effect, all in the same loop as the \ left tyre animation for maximum efficiency LDX #4 \ Set a loop counter to go from 4 to 0 .tyre1 LDA tyreLeft3,X \ Set tyreLeft3 = tyreLeft3 EOR tyreTreadLeft EOR tyreTreadLeft,X \ to flip the pixels in the bottom-left of the tyre STA tyreLeft3,X LDA tyreRight3,X \ Set tyreRight3 = tyreRight3 EOR tyreTreadRight EOR tyreTreadRight,X \ to flip the pixels in the bottom-right of the tyre STA tyreRight3,X CPX #3 \ If X >= 3, jump to tyre2 to skip the following BCS tyre2 LDA tyreLeft1,X \ Flip all four pixels at tyreLeft1+X, so that's EOR #%11110000 \ tyreLeft1+2, tyreLeft1+1 and tyreLeft1 STA tyreLeft1,X LDA tyreRight1,X \ Flip all four pixels at tyreRight1+X, so that's EOR #%11110000 \ tyreRight1+2, tyreRight1+1 and tyreRight1 STA tyreRight1,X BNE tyre3 \ Jump to tyre3 to continue the loop (this BNE is \ effectively a JMP as A is never zero, because the \ screen byte in A is a chequered pattern) .tyre2 LDA tyreLeft2,X \ Flip the two left pixels at tyreLeft2+X, so that's EOR #%11000000 \ tyreLeft2+4 and tyreLeft2+3 STA tyreLeft2,X LDA tyreRight2,X \ Flip the two right pixels at tyreRight2+X, so that's EOR #%00110000 \ tyreRight2+4 and tyreRight2+3 STA tyreRight2,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 \ Type: Variable \ Category: Dashboard \ Summary: Tyre tread pattern for the left tyre \ \ ------------------------------------------------------------------------------ \ \ Contains the shape of the bottom part of the tread on the left tyre, which we \ animate when the car is moving. \ \ ****************************************************************************** .tyreTreadLeft EQUB %11110000 EQUB %11110000 EQUB %11000000 EQUB %11000000 EQUB %10000000 \ ****************************************************************************** \ \ Name: tyreTreadRight \ Type: Variable \ Category: Dashboard \ Summary: Tyre tread pattern for the right tyre \ \ ------------------------------------------------------------------------------ \ \ Contains the shape of the bottom part of the tread on the right tyre, which we \ animate when the car is moving. \ \ ****************************************************************************** .tyreTreadRight EQUB %11110000 EQUB %11110000 EQUB %00110000 EQUB %00110000 EQUB %00010000 \ ****************************************************************************** \ \ Name: trackData \ 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 \ \ ------------------------------------------------------------------------------ \ \ 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, enable 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 \ Type: Variable \ Category: Screen buffer \ Summary: Contains part of the dashboard image that gets moved into screen \ memory \ Deep dive: The jigsaw puzzle binary \ \ ****************************************************************************** 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 \ 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 \ Type: Subroutine \ Category: Setup \ Summary: The track file's hook code \ Deep dive: The track data file format \ \ ****************************************************************************** .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 \ Type: Subroutine \ Category: Drivers \ Summary: Award points following a race \ \ ------------------------------------------------------------------------------ \ \ 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 \ ****************************************************************************** \ \ Name: xVergeRightLo \ Type: Variable \ Category: Track geometry \ Summary: Low byte of segment yaw angles along the right track verge in \ front of the player (i.e. along the x-axis) \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (xVergeRightHi xVergeRightLo). \ \ ****************************************************************************** ORG &5E40 .xVergeRightLo SKIP 40 \ ****************************************************************************** \ \ Name: xVergeLeftLo \ Type: Variable \ Category: Track geometry \ Summary: Low byte of segment yaw angles along the left track verge in front \ of the player (i.e. along the x-axis) \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (xVergeLeftHi xVergeLeftLo). \ \ ****************************************************************************** .xVergeLeftLo SKIP 40 \ ****************************************************************************** \ \ Name: xVergeRightHi \ Type: Variable \ Category: Track geometry \ Summary: High byte of segment yaw angles along the right track verge in \ front of the player (i.e. along the x-axis) \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (xVergeRightHi xVergeRightLo). \ \ ****************************************************************************** .xVergeRightHi SKIP 40 \ ****************************************************************************** \ \ Name: xVergeLeftHi \ Type: Variable \ Category: Track geometry \ Summary: High byte of segment yaw angles along the left track verge in \ front of the player (i.e. along the x-axis) \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (xVergeLeftHi xVergeLeftLo). \ \ ****************************************************************************** .xVergeLeftHi SKIP 40 \ ****************************************************************************** \ \ Name: vergeDataRight \ Type: Variable \ Category: Track geometry \ Summary: Data (such as colour) for the verge marks on the right side of the \ track \ \ ------------------------------------------------------------------------------ \ \ * Bits 0-1: colour of the verge mark \ \ * 0 = black \ * 1 = red \ * 2 = white \ \ * Bit 7 gets set in MapSegmentsToLines when this segment is hidden behind a \ hill \ \ ****************************************************************************** .vergeDataRight SKIP 24 \ ****************************************************************************** \ \ Name: scaledScaffold \ Type: Variable \ Category: 3D objects \ Summary: Storage for an object's scaled scaffold \ Deep dive: Scaling objects with scaffolds \ \ ****************************************************************************** .scaledScaffold SKIP 16 \ ****************************************************************************** \ \ Name: vergeDataLeft \ Type: Variable \ Category: Track geometry \ Summary: Data (such as colour) for the verge marks on the left side of the \ track \ \ ------------------------------------------------------------------------------ \ \ * Bits 0-1: colour of the verge mark \ \ * 0 = black \ * 1 = red \ * 2 = white \ \ * Bit 7: gets set in MapSegmentsToLines \ \ ****************************************************************************** .vergeDataLeft SKIP 24 \ ****************************************************************************** \ \ Name: yVergeRight \ Type: Variable \ Category: Track geometry \ Summary: Segment pitch angles along the right track verge in front of \ the player (i.e. along the up-down y-axis) \ \ ****************************************************************************** .yVergeRight SKIP 24 \ ****************************************************************************** \ \ Name: numberOfPlayers \ Type: Variable \ Category: Drivers \ Summary: The number of players \ \ ****************************************************************************** .numberOfPlayers SKIP 1 \ ****************************************************************************** \ \ Name: lowestPlayerNumber \ Type: Variable \ Category: Drivers \ Summary: The number of the player with the lowest player number \ \ ****************************************************************************** .lowestPlayerNumber SKIP 1 \ Contains 20 minus the number of players, so that's: \ \ * 19 if there is one player \ * 18 if there are two players \ \ and so on, down to 0 if there are 20 players \ \ Human players take the place of drivers with higher \ numbers, so the first player takes the place of driver \ 19 (the aptly called Dummy Driver, as they never get \ to race), and the second player takes the place of \ driver 18 (Peter Out), the third player replaces \ driver 17 (Rick Shaw) and so on \ \ So this not only represents the lowest player number, \ but also the highest non-human driver number (which is \ lowestPlayerNumber - 1) \ ****************************************************************************** \ \ Name: raceClass \ Type: Variable \ Category: Drivers \ Summary: The class of the current race \ \ ****************************************************************************** .raceClass SKIP 1 \ The class of race: \ \ * 0 = Novice \ \ * 1 = Amateur \ \ * 2 = Professional \ ****************************************************************************** \ \ Name: qualifyingTime \ Type: Variable \ Category: Drivers \ Summary: The number of minutes of qualifying lap time \ \ ****************************************************************************** .qualifyingTime SKIP 1 \ The number of minutes of qualifying lap time, minus \ one, as follows: \ \ * 4 gives us 5 minutes of qualifying time \ \ * 9 gives us 10 minutes of qualifying time \ \ * 25 gives us 26 minutes of qualifying time \ \ * 255 gives us infinite time (for practice) \ \ Note that the third value should be 19 to match the \ menu option of 20 minutes, but the value in the \ timeFromOption table is incorrect \ ****************************************************************************** \ \ Name: competitionStarted \ Type: Variable \ Category: Drivers \ Summary: A flag to indicate whether or not the competition has started \ \ ****************************************************************************** .competitionStarted SKIP 1 \ Flag to indicate whether the competition has started: \ \ * 0 = the competition has not started \ \ * Non-zero = the competition has started \ ****************************************************************************** \ \ Name: frontWingSetting \ Type: Variable \ Category: Driving model \ Summary: The front wing setting, as entered by the player \ \ ****************************************************************************** .frontWingSetting SKIP 1 \ ****************************************************************************** \ \ Name: rearWingSetting \ Type: Variable \ Category: Driving model \ Summary: The rear wing setting, as entered by the player \ \ ****************************************************************************** .rearWingSetting SKIP 1 \ ****************************************************************************** \ \ Name: lapsMenuOption \ Type: Variable \ Category: Drivers \ Summary: The menu option chosen from the laps menu (0 to 2) \ \ ****************************************************************************** .lapsMenuOption SKIP 1 \ ****************************************************************************** \ \ Name: baseSpeed \ Type: Variable \ Category: Drivers \ Summary: The base speed for each car, copied from the track data \ \ ****************************************************************************** .baseSpeed SKIP 1 \ The base speed for each car, which is faster with a \ higher class of race (this value is taken from the \ track data at trackBaseSpeed): \ \ * 134 = Novice \ \ * 146 = Amateur \ \ * 152 = Professional SKIP 7 \ ****************************************************************************** \ \ Name: yVergeLeft \ Type: Variable \ Category: Track geometry \ Summary: Segment pitch angles along the left track verge in front of \ the player (i.e. along the up-down y-axis) \ \ ****************************************************************************** .yVergeLeft SKIP 24 \ ****************************************************************************** \ \ Name: backgroundColour \ Type: Variable \ Category: Screen buffer \ Summary: The background colour for each track line \ Deep dive: Drawing the track view \ \ ------------------------------------------------------------------------------ \ \ This table contains colour information for each of the 80 track lines. It \ stores the colour, the verge type and details of the routine in which the \ colour data was written, as follows: \ \ * Bits 0-1: background colour of track line (this value is a logical colour \ and the physical colour is looked up from the colourPalette table) \ \ %00 = logical colour 0 \ %01 = logical colour 1 \ %10 = logical colour 2 \ %11 = logical colour 3 \ \ * Bit 2: records which routine set this colour, along with bits 5-7 \ \ %0 = not set by SetVergeBackground \ %1 = set by SetVergeBackground \ \ * Bits 3-4: the verge type that was being drawn when this colour was set \ (taken from vergeType) \ \ %00 = leftVergeStart \ %01 = leftTrackStart \ %10 = rightVergeStart \ %11 = rightGrassStart \ \ * Bits 5-7: records which routine set this colour, along with bit 2 \ \ %000 = colour not set yet \ %001 = set by SetBackground \ %010 = set by UpdateBackground to the value in backgroundRight \ %100 = set by UpdateBackground to the value in backgroundLeft \ \ The contents of bits 2-7 are only used by the GetColour routine in the \ Acornsoft version (though their being non-zero is used to differentiate black \ from an unset colour by SetBackground). In the Superior Software release, \ GetColour was recoded to be smaller, and the recoded version only uses bits \ 0-1, with bits 2-7 being unused. \ \ ****************************************************************************** .backgroundColour SKIP 80 \ ****************************************************************************** \ \ Name: sectionSteering \ Type: Variable \ Category: Tactics \ Summary: The optimum steering for each section \ Deep dive: Tactics of the non-player drivers \ \ ****************************************************************************** .sectionSteering SKIP 26 \ The carSteering value to steer round the corner for a \ track section \ \ The various bits are as for carSteering: \ \ * Bits 0-5 = the amount of steering as a positive \ value (0 to 31) \ \ * Bit 6 = controls whether to apply steering in the \ MoveCars routine \ \ * Clear = always apply steering \ \ * Set = only apply steering if there is enough \ room on the track \ \ * Bit 7 = the direction of the steering \ \ * Clear = steer left \ \ * Set = steer right EQUB &00, &00 \ These bytes appear to be unused EQUB &00, &00 EQUB &00, &00 \ ****************************************************************************** \ \ Name: vergePixelMask \ Type: Variable \ Category: Drawing the track \ Summary: Pixel bytes for drawing track verge edges \ \ ------------------------------------------------------------------------------ \ \ This table contains four bytes for each of the different colour schemes for \ the track verges. \ \ The first byte contains four pixels of the fill colour, which gets poked into \ the byte to the right of the edge byte we are drawing. \ \ The other three bytes contain 1, 2 and 3 pixels of the foreground colour, with \ the pixels coming in from the left. So each batch looks like this if all four \ pixels are drawn: \ \ .... .... \ x... .... \ xx.. .... \ xxx. .... \ \ In the comments, this would be denoted as "Colour x then ." as colour x is on \ the left, then colour . is on the right. \ \ ****************************************************************************** .vergePixelMask EQUB %00000000 \ Colour 3 then 0 (green then black) bbbb bbbb EQUB %10001000 \ gbbb bbbb EQUB %11001100 \ * pixelMaskVerge for leftVergeStart ggbb bbbb EQUB %11101110 \ * pixelMaskNoVerge for leftTrackStart gggb bbbb EQUB %00001111 \ Colour 3 then 1 (green then red) rrrr rrrr EQUB %10001111 \ grrr rrrr EQUB %11001111 \ ggrr rrrr EQUB %11101111 \ gggr rrrr EQUB %11110000 \ Colour 3 then 2 (green then white) wwww wwww EQUB %11111000 \ gwww wwww EQUB %11111100 \ * pixelMaskVerge for leftTrackStart ggww wwww EQUB %11111110 \ gggw wwww EQUB %00000000 \ Colour 1 then 0 (red then black) bbbb bbbb EQUB %00001000 \ rbbb bbbb EQUB %00001100 \ rrbb bbbb EQUB %00001110 \ rrrb bbbb EQUB %00000000 \ Colour 2 then 0 (white then black) bbbb bbbb EQUB %10000000 \ wbbb bbbb EQUB %11000000 \ * pixelMaskVerge for rightVergeStart wwbb bbbb EQUB %11100000 \ wwwb bbbb EQUB %00001111 \ Colour 0 then 1 (black then red) rrrr rrrr EQUB %00000111 \ brrr rrrr EQUB %00000011 \ bbrr rrrr EQUB %00000001 \ bbbr rrrr EQUB %11110000 \ Colour 0 then 2 (black then white) wwww wwww EQUB %01110000 \ bwww wwww EQUB %00110000 \ bbww wwww EQUB %00010000 \ bbbw wwww EQUB %11111111 \ Colour 0 then 3 (black then green) gggg gggg EQUB %01110111 \ bggg gggg EQUB %00110011 \ * pixelMaskVerge for rightGrassStart bbgg gggg EQUB %00010001 \ * pixelMaskNoVerge for rightVergeStart bbbg gggg \ * pixelMaskNoVerge for rightGrassStart EQUB %11111111 \ Colour 1 then 3 (red then green) gggg gggg EQUB %01111111 \ rggg gggg EQUB %00111111 \ rrgg gggg EQUB %00011111 \ rrrg gggg EQUB %11111111 \ Colour 2 then 3 (white then green) gggg gggg EQUB %11110111 \ wggg gggg EQUB %11110011 \ wwgg gggg EQUB %11110001 \ wwwg gggg EQUB &03, &60 \ These bytes appear to be unused \ ****************************************************************************** \ \ Name: scaleRange \ Type: Variable \ Category: 3D objects \ Summary: Storage for scale factors when scaling objects \ \ ****************************************************************************** .scaleRange IF _ACORNSOFT OR _4TRACKS EQUB &6F, &6E \ These values are workspace noise and have no meaning EQUB &32, &00 EQUB &8D, &2B ELIF _SUPERIOR OR _REVSPLUS EQUB &30, &18 \ These values are workspace noise and have no meaning EQUB &0C, &06 EQUB &03, &01 ENDIF \ ****************************************************************************** \ \ Name: zeroIfYIs55 \ Type: Variable \ Category: Screen buffer \ Summary: A lookup table for zeroing Y if and only if it is &55 \ \ ****************************************************************************** .zeroIfYIs55 FOR I%, 0, 255 IF I% = &55 B% = 0 ELSE B% = I% ENDIF EQUB B% NEXT \ ****************************************************************************** \ \ Name: arctanY \ Type: Variable \ Category: Maths (Geometry) \ Summary: Table for arctan values when calculating yaw angles \ Deep dive: Trigonometry \ \ ****************************************************************************** .arctanY FOR I%, 0, 255 EQUB INT(0.5 + ATN(I% / 256) * 256 / ATN(1)) NEXT \ ****************************************************************************** \ \ Name: divideX \ Type: Variable \ Category: Maths (Arithmetic) \ Summary: Division table for calculating scale factors using 1 / |x-delta| \ Deep dive: Trigonometry \ \ ****************************************************************************** .divideX FOR I%, 0, 127 IF I% = 0 B% = 255 ELSE B% = INT(0.5 + 256 / (1 + I% / 128)) ENDIF EQUB B% NEXT \ ****************************************************************************** \ \ Name: xPlayerCoordHi \ Type: Variable \ Category: Car geometry \ Summary: The high byte of the x-coordinate of the player's 3D coordinates \ \ ------------------------------------------------------------------------------ \ \ The coordinate is stored as a 24-bit number in (xPlayerCoordTop xPlayerCoordHi \ xPlayerCoordLo). \ \ ****************************************************************************** .xPlayerCoordHi EQUB 0 \ ****************************************************************************** \ \ Name: yPlayerCoordHi \ Type: Variable \ Category: Car geometry \ Summary: The high byte of the y-coordinate of the player's 3D coordinates \ \ ------------------------------------------------------------------------------ \ \ The coordinate is stored as a 24-bit number in (yPlayerCoordTop yPlayerCoordHi \ yPlayerCoordLo). \ \ ****************************************************************************** .yPlayerCoordHi EQUB 0 \ ****************************************************************************** \ \ Name: zPlayerCoordHi \ Type: Variable \ Category: Car geometry \ Summary: The high byte of the z-coordinate of the player's 3D coordinates \ \ ------------------------------------------------------------------------------ \ \ The coordinate is stored as a 24-bit number in (zPlayerCoordTop zPlayerCoordHi \ zPlayerCoordLo). \ \ ****************************************************************************** .zPlayerCoordHi EQUB 0 \ ****************************************************************************** \ \ Name: xPlayerCoordTop \ Type: Variable \ Category: Car geometry \ Summary: The top byte of the x-coordinate of the player's 3D coordinates \ \ ------------------------------------------------------------------------------ \ \ The coordinate is stored as a 24-bit number in (xPlayerCoordTop xPlayerCoordHi \ xPlayerCoordLo). \ \ ****************************************************************************** .xPlayerCoordTop EQUB 0 \ ****************************************************************************** \ \ Name: yPlayerCoordTop \ Type: Variable \ Category: Car geometry \ Summary: The top byte of the y-coordinate of the player's 3D coordinates \ \ ------------------------------------------------------------------------------ \ \ The coordinate is stored as a 24-bit number in (yPlayerCoordTop yPlayerCoordHi \ yPlayerCoordLo). \ \ ****************************************************************************** .yPlayerCoordTop EQUB 0 \ ****************************************************************************** \ \ Name: zPlayerCoordTop \ Type: Variable \ Category: Car geometry \ Summary: The top byte of the z-coordinate of the player's 3D coordinates \ \ ------------------------------------------------------------------------------ \ \ The coordinate is stored as a 24-bit number in (zPlayerCoordTop zPlayerCoordHi \ zPlayerCoordLo). \ \ ****************************************************************************** .zPlayerCoordTop EQUB 0 \ ****************************************************************************** \ \ Name: xRoadSignCoordLo \ Type: Variable \ Category: 3D objects \ Summary: The low byte of the x-coordinate of the road sign's 3D coordinates \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (xRoadSignCoordHi xRoadSignCoordLo). \ \ ****************************************************************************** .xRoadSignCoordLo EQUB 0 \ ****************************************************************************** \ \ Name: yRoadSignCoordLo \ Type: Variable \ Category: 3D objects \ Summary: The low byte of the y-coordinate of the road sign's 3D coordinates \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (yRoadSignCoordHi yRoadSignCoordLo). \ \ ****************************************************************************** .yRoadSignCoordLo EQUB 0 \ ****************************************************************************** \ \ Name: zRoadSignCoordLo \ Type: Variable \ Category: 3D objects \ Summary: The low byte of the z-coordinate of the road sign's 3D coordinates \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (zRoadSignCoordHi zRoadSignCoordLo). \ \ ****************************************************************************** .zRoadSignCoordLo EQUB 0 \ ****************************************************************************** \ \ Name: xRoadSignCoordHi \ Type: Variable \ Category: 3D objects \ Summary: The high byte of the x-coordinate of the road sign's 3D \ coordinates \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (xRoadSignCoordHi xRoadSignCoordLo). \ \ ****************************************************************************** .xRoadSignCoordHi EQUB 0 \ ****************************************************************************** \ \ Name: yRoadSignCoordHi \ Type: Variable \ Category: 3D objects \ Summary: The high byte of the y-coordinate of the road sign's 3D \ coordinates \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (yRoadSignCoordHi yRoadSignCoordLo). \ \ ****************************************************************************** .yRoadSignCoordHi EQUB 0 \ ****************************************************************************** \ \ Name: zRoadSignCoordHi \ Type: Variable \ Category: 3D objects \ Summary: The high byte of the z-coordinate of the road sign's 3D \ coordinates \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (zRoadSignCoordHi zRoadSignCoordLo). \ \ ****************************************************************************** .zRoadSignCoordHi EQUB 0 EQUB &00, &00 \ These bytes appear to be unused EQUB &00 \ ****************************************************************************** \ \ Name: objectPalette \ Type: Variable \ Category: 3D objects \ Summary: The object colour palette that maps logical colours 0 to 3 to \ physical colours \ \ ------------------------------------------------------------------------------ \ \ This table is also used for storing pixel bytes when drawing edges of the \ track verges. \ \ ****************************************************************************** .objectPalette EQUB 0 EQUB 0 EQUB 0 EQUB 0 \ ****************************************************************************** \ \ Name: mirrorContents \ Type: Variable \ Category: Dashboard \ Summary: Contains the size of the car in each mirror segment \ Deep dive: Wing mirrors \ \ ****************************************************************************** .mirrorContents EQUB 0 \ Mirror segment 0 (left mirror, outer segment) EQUB 0 \ Mirror segment 1 (left mirror, middle segment) EQUB 0 \ Mirror segment 2 (left mirror, inner segment) EQUB 0 \ Mirror segment 3 (right mirror, inner segment) EQUB 0 \ Mirror segment 4 (right mirror, middle segment) EQUB 0 \ Mirror segment 5 (right mirror, outer segment) \ ****************************************************************************** \ \ Name: markerData \ Type: Variable \ Category: Track geometry \ Summary: The segment flags for each of the three corner markers \ \ ****************************************************************************** .markerData EQUB 0 EQUB 0 EQUB 0 \ ****************************************************************************** \ \ Name: vergeEdgeRight \ Type: Variable \ Category: Drawing the track \ Summary: Contain the four pixel bytes for the verge edge we are drawing, \ masked to only include the rightmost 4, 3, 2 and 1 pixels \ \ ****************************************************************************** .vergeEdgeRight EQUB 0 \ Includes all four pixels xxxx EQUB 0 \ Includes pixels 1, 2, 3 .xxx EQUB 0 \ Includes pixels 2, 3 ..xx EQUB 0 \ Includes pixel 3 ...x \ ****************************************************************************** \ \ Name: sinYawAngleLo \ Type: Variable \ Category: Driving model \ Summary: Low byte of the sine of the player's yaw angle \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (sinYawAngleHi sinYawAngleLo). \ \ ****************************************************************************** .sinYawAngleLo EQUB 0 \ ****************************************************************************** \ \ Name: cosYawAngleLo \ Type: Variable \ Category: Driving model \ Summary: Low byte of the cosine of the player's yaw angle \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (cosYawAngleHi cosYawAngleLo). \ \ ****************************************************************************** .cosYawAngleLo EQUB 0 \ ****************************************************************************** \ \ Name: steeringLo \ Type: Variable \ Category: Dashboard \ Summary: The low byte of the steering wheel position \ Deep dive: Computer assisted steering (CAS) \ \ ------------------------------------------------------------------------------ \ \ The steering wheel position is stored as (steeringHi steeringLo), with the \ sign bit in bit 0 of steeringLo, so it's a sign-magnitude number. \ \ Negative (bit 0 set) means we are steering left, positive (bit 0 clear) means \ we are steering right. \ \ ****************************************************************************** .steeringLo EQUB 0 \ ****************************************************************************** \ \ Name: sinYawAngleHi \ Type: Variable \ Category: Driving model \ Summary: High byte of the sine of the player's yaw angle \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (sinYawAngleHi sinYawAngleLo). \ \ ****************************************************************************** .sinYawAngleHi EQUB 0 \ ****************************************************************************** \ \ Name: cosYawAngleHi \ Type: Variable \ Category: Driving model \ Summary: High byte of the cosine of the player's yaw angle \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (cosYawAngleHi cosYawAngleLo). \ \ ****************************************************************************** .cosYawAngleHi EQUB 0 \ ****************************************************************************** \ \ Name: steeringHi \ Type: Variable \ Category: Dashboard \ Summary: The high byte of the steering wheel position \ Deep dive: Computer assisted steering (CAS) \ \ ------------------------------------------------------------------------------ \ \ The steering wheel position is stored as (steeringHi steeringLo), with the \ sign bit in bit 0 of steeringLo, so it's a sign-magnitude number. \ \ Negative (bit 0 set) means we are steering left, positive (bit 0 clear) means \ we are steering right. \ \ ****************************************************************************** .steeringHi EQUB 0 \ ****************************************************************************** \ \ Name: tyreSqueal \ Type: Variable \ Category: Driving model \ Summary: A flag to determine whether the front or rear tyres are squealing \ \ ------------------------------------------------------------------------------ \ \ If bit 7 is set in either of these, we make the sound of squealing tyres. \ \ ****************************************************************************** .tyreSqueal EQUB 0 \ Front tyres EQUB 0 \ Rear tyres \ ****************************************************************************** \ \ Name: wingSetting \ Type: Variable \ Category: Driving model \ Summary: Contains the scaled wing settings \ \ ****************************************************************************** .wingSetting EQUB 0 \ Front wing setting, scaled to the range 90 to 218 EQUB 0 \ Rear wing setting, scaled to the range 90 to 218 \ ****************************************************************************** \ \ Name: wingForce \ Type: Variable \ Category: Driving model \ Summary: The downward force from the front and rear wings \ \ ****************************************************************************** .wingForce EQUB 0 \ Front wing EQUB 0 \ Rear wing \ ****************************************************************************** \ \ Name: wingForce95 \ Type: Variable \ Category: Driving model \ Summary: 95% of the downward force from the front and rear wings \ \ ****************************************************************************** .wingForce95 EQUB 0 \ Front wing EQUB 0 \ Rear wing \ ****************************************************************************** \ \ Name: xPlayerSpeedLo \ Type: Variable \ Category: Driving model \ Summary: Low byte of the x-coordinate of the velocity vector (i.e. x-axis \ speed) for the player's car during this main loop iteration \ \ ------------------------------------------------------------------------------ \ \ The speed is stored as a 24-bit number in (xPlayerSpeedTop xPlayerSpeedHi \ xPlayerSpeedLo). \ \ ****************************************************************************** .xPlayerSpeedLo EQUB 0 \ ****************************************************************************** \ \ Name: zPlayerSpeedLo \ Type: Variable \ Category: Driving model \ Summary: Low byte of the z-coordinate of the velocity vector (i.e. z-axis \ speed) for the player's car during this main loop iteration \ \ ------------------------------------------------------------------------------ \ \ The speed is stored as a 24-bit number in (zPlayerSpeedTop zPlayerSpeedHi \ zPlayerSpeedLo). \ \ ****************************************************************************** .zPlayerSpeedLo EQUB 0 \ ****************************************************************************** \ \ Name: spinYawAngleLo \ Type: Variable \ Category: Car geometry \ Summary: Low byte of the amount of yaw angle spin that is being applied to \ the player's car \ \ ------------------------------------------------------------------------------ \ \ The spin is stored as a 24-bit number in (spinYawAngleTop spinYawAngleHi \ spinYawAngleLo). \ \ ****************************************************************************** .spinYawAngleLo EQUB 0 \ ****************************************************************************** \ \ Name: xPlayerCoordLo \ Type: Variable \ Category: Car geometry \ Summary: The low byte of the x-coordinate of the player's 3D coordinates \ \ ------------------------------------------------------------------------------ \ \ The coordinate is stored as a 24-bit number in (xPlayerCoordTop xPlayerCoordHi \ xPlayerCoordLo). \ \ ****************************************************************************** .xPlayerCoordLo EQUB 0 \ ****************************************************************************** \ \ Name: yPlayerCoordLo \ Type: Variable \ Category: Car geometry \ Summary: The low byte of the y-coordinate of the player's 3D coordinates \ \ ------------------------------------------------------------------------------ \ \ The coordinate is stored as a 24-bit number in (yPlayerCoordTop yPlayerCoordHi \ yPlayerCoordLo). \ \ ****************************************************************************** .yPlayerCoordLo EQUB 0 \ ****************************************************************************** \ \ Name: zPlayerCoordLo \ Type: Variable \ Category: Car geometry \ Summary: The low byte of the z-coordinate of the player's 3D coordinates \ \ ------------------------------------------------------------------------------ \ \ The coordinate is stored as a 24-bit number in (zPlayerCoordTop zPlayerCoordHi \ zPlayerCoordLo). \ \ ****************************************************************************** .zPlayerCoordLo EQUB 0 \ ****************************************************************************** \ \ Name: markerListIndex \ Type: Variable \ Category: Track geometry \ Summary: The index of the corresponding entry in the track segment list for \ each of the three corner markers \ \ ****************************************************************************** .markerListIndex EQUB 0 EQUB 0 EQUB 0 \ ****************************************************************************** \ \ Name: xMarkerLo \ Type: Variable \ Category: Track geometry \ Summary: Low byte of the x-axis distance between the track edge and the \ three corner markers \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (xMarkerHi xMarkerLo). \ \ ****************************************************************************** .xMarkerLo EQUB 0 EQUB 0 EQUB 0 \ ****************************************************************************** \ \ Name: xMarkerHi \ Type: Variable \ Category: Track geometry \ Summary: High byte of the x-axis distance between the track edge and the \ three corner markers \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (xMarkerHi xMarkerLo). \ \ ****************************************************************************** .xMarkerHi EQUB 0 EQUB 0 EQUB 0 \ ****************************************************************************** \ \ Name: soundBuffer \ Type: Variable \ Category: Sound \ Summary: Details of each sound channel's buffer status \ Deep dive: The engine sounds \ \ ****************************************************************************** .soundBuffer EQUB 0 EQUB 0 EQUB 0 EQUB 0 EQUB 0 EQUB 0 \ ****************************************************************************** \ \ Name: characterDef \ Type: Variable \ Category: Text \ Summary: Storage for a character definition when printing characters \ \ ****************************************************************************** .characterDef EQUB 0 \ The character number EQUB 0, 0, 0, 0 \ The eight bytes that make up the character definition, EQUB 0, 0, 0, 0 \ which gets populated by an OSWORD call \ ****************************************************************************** \ \ Name: xCursor \ Type: Variable \ Category: Text \ Summary: The cursor's x-coordinate, which can either be a pixel coordinate \ or a character row \ \ ****************************************************************************** .xCursor EQUB 0 \ ****************************************************************************** \ \ Name: yCursor \ Type: Variable \ Category: Text \ Summary: The cursor's pixel y-coordinate \ \ ------------------------------------------------------------------------------ \ \ In terms of printing text on-screen, we need to set: \ \ * yCursor = 24 for the first line of text \ \ * yCursor = 33 for the second line of text \ \ See the notes on the yLookupHi variable for information about the values of \ yCursor and how they relate to the custom screen. \ \ ****************************************************************************** .yCursor EQUB 0 EQUB &00, &00 \ These bytes appear to be unused \ ****************************************************************************** \ \ Name: xPlayerSpeedHi \ Type: Variable \ Category: Driving model \ Summary: High byte of the x-coordinate of the velocity vector (i.e. x-axis \ speed) for the player's car during this main loop iteration \ \ ------------------------------------------------------------------------------ \ \ The speed is stored as a 24-bit number in (xPlayerSpeedTop xPlayerSpeedHi \ xPlayerSpeedLo). \ \ ****************************************************************************** .xPlayerSpeedHi EQUB 0 \ ****************************************************************************** \ \ Name: zPlayerSpeedHi \ Type: Variable \ Category: Driving model \ Summary: High byte of the z-coordinate of the velocity vector (i.e. z-axis \ speed) for the player's car during this main loop iteration \ \ ------------------------------------------------------------------------------ \ \ The speed is stored as a 24-bit number in (zPlayerSpeedTop zPlayerSpeedHi \ zPlayerSpeedLo). \ \ ****************************************************************************** .zPlayerSpeedHi EQUB 0 \ ****************************************************************************** \ \ Name: spinYawAngleHi \ Type: Variable \ Category: Car geometry \ Summary: High byte of the amount of yaw angle spin that is being applied to \ the player's car \ \ ------------------------------------------------------------------------------ \ \ The spin is stored as a 24-bit number in (spinYawAngleTop spinYawAngleHi \ spinYawAngleLo). \ \ ****************************************************************************** .spinYawAngleHi EQUB 0 \ ****************************************************************************** \ \ Name: xAccelerationLo \ Type: Variable \ Category: Driving model \ Summary: Low byte of the x-coordinate of the change in velocity of the \ player's car in terms of 3D world coordinates \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (xAccelerationHi xAccelerationLo). \ \ ****************************************************************************** .xAccelerationLo EQUB 0 \ ****************************************************************************** \ \ Name: zAccelerationLo \ Type: Variable \ Category: Driving model \ Summary: Low byte of the z-coordinate of the change in velocity of the \ player's car in terms of 3D world coordinates \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (zAccelerationHi zAccelerationLo). \ \ ****************************************************************************** .zAccelerationLo EQUB 0 \ ****************************************************************************** \ \ Name: spinYawDeltaLo \ Type: Variable \ Category: Driving model \ Summary: Low byte of the change in the spin yaw angle for the player's car \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (spinYawDeltaHi spinYawDeltaLo). \ \ ****************************************************************************** .spinYawDeltaLo EQUB 0 \ ****************************************************************************** \ \ Name: xPlayerAccelLo \ Type: Variable \ Category: Driving model \ Summary: Low byte of the x-coordinate of the change in velocity of the \ player's car in the player's frame of reference \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (xPlayerAccelHi xPlayerAccelLo). \ \ ****************************************************************************** .xPlayerAccelLo EQUB 0 \ ****************************************************************************** \ \ Name: zPlayerAccelLo \ Type: Variable \ Category: Driving model \ Summary: Low byte of the z-coordinate of the change in velocity of the \ player's car in the player's frame of reference \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (zPlayerAccelHi zPlayerAccelLo). \ \ ****************************************************************************** .zPlayerAccelLo EQUB 0 \ ****************************************************************************** \ \ Name: xVelocityLo \ Type: Variable \ Category: Driving model \ Summary: Low byte of the x-coordinate of the player's velocity vector, \ from the player's frame of reference \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (xVelocityHi xVelocityLo). \ \ ****************************************************************************** .xVelocityLo EQUB 0 \ ****************************************************************************** \ \ Name: zVelocityLo \ Type: Variable \ Category: Driving model \ Summary: Low byte of the z-coordinate of the player's velocity vector, \ from the player's frame of reference \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (zVelocityHi zVelocityLo). \ \ ****************************************************************************** .zVelocityLo EQUB 0 \ ****************************************************************************** \ \ Name: xTyreForceNoseLo \ Type: Variable \ Category: Driving model \ Summary: Low byte of the x-coordinate of the force vector produced by the \ front tyres in the nose of the car \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (xTyreForceNoseHi xTyreForceNoseLo). \ \ ****************************************************************************** .xTyreForceNoseLo EQUB 0 \ ****************************************************************************** \ \ Name: xTyreForceRearLo \ Type: Variable \ Category: Driving model \ Summary: Low byte of the x-coordinate of the force vector produced by the \ rear tyres of the car \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (xTyreForceRearHi xTyreForceRearLo). \ \ ****************************************************************************** .xTyreForceRearLo EQUB 0 \ ****************************************************************************** \ \ Name: zTyreForceNoseLo \ Type: Variable \ Category: Driving model \ Summary: Low byte of the z-coordinate of the force vector produced by the \ front tyres in the nose of the car \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (zTyreForceNoseHi zTyreForceNoseLo). \ \ ****************************************************************************** .zTyreForceNoseLo EQUB 0 \ ****************************************************************************** \ \ Name: zTyreForceRearLo \ Type: Variable \ Category: Driving model \ Summary: Low byte of the z-coordinate of the force vector produced by the \ rear tyres \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (zTyreForceRearHi zTyreForceRearLo). \ \ ****************************************************************************** .zTyreForceRearLo EQUB 0 \ ****************************************************************************** \ \ Name: xSteeringForceLo \ Type: Variable \ Category: Driving model \ Summary: Low byte of the sideways force applied by steering \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (xSteeringForceHi xSteeringForceLo). \ \ ****************************************************************************** .xSteeringForceLo EQUB 0 \ ****************************************************************************** \ \ Name: mainLoopCounterHi \ Type: Variable \ Category: Main loop \ Summary: High byte of the main loop counter, which increments on each \ iteration of the main driving loop \ Deep dive: Scheduling tasks in the main loop \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (mainLoopCounterHi mainLoopCounterLo). \ \ ****************************************************************************** .mainLoopCounterHi EQUB 0 \ ****************************************************************************** \ \ Name: xPlayerSpeedTop \ Type: Variable \ Category: Driving model \ Summary: Top byte of the x-coordinate of the velocity vector (i.e. x-axis \ speed) for the player's car during this main loop iteration \ \ ------------------------------------------------------------------------------ \ \ The speed is stored as a 24-bit number in (xPlayerSpeedTop xPlayerSpeedHi \ xPlayerSpeedLo). \ \ ****************************************************************************** .xPlayerSpeedTop EQUB 0 \ ****************************************************************************** \ \ Name: zPlayerSpeedTop \ Type: Variable \ Category: Driving model \ Summary: Top byte of the z-coordinate of the velocity vector (i.e. z-axis \ speed) for the player's car during this main loop iteration \ \ ------------------------------------------------------------------------------ \ \ The speed is stored as a 24-bit number in (zPlayerSpeedTop zPlayerSpeedHi \ zPlayerSpeedLo). \ \ ****************************************************************************** .zPlayerSpeedTop EQUB 0 \ ****************************************************************************** \ \ Name: spinYawAngleTop \ Type: Variable \ Category: Car geometry \ Summary: Top byte of the amount of yaw angle spin that is being applied \ to the player's car \ \ ------------------------------------------------------------------------------ \ \ The spin is stored as a 24-bit number in (spinYawAngleTop spinYawAngleHi \ spinYawAngleLo). \ \ ****************************************************************************** .spinYawAngleTop EQUB 0 \ ****************************************************************************** \ \ Name: xAccelerationHi \ Type: Variable \ Category: Driving model \ Summary: High byte of the x-coordinate of the change in velocity of the \ player's car in terms of 3D world coordinates \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (xAccelerationHi xAccelerationLo). \ \ ****************************************************************************** .xAccelerationHi EQUB 0 \ ****************************************************************************** \ \ Name: zAccelerationHi \ Type: Variable \ Category: Driving model \ Summary: High byte of the z-coordinate of the change in velocity of the \ player's car in terms of 3D world coordinates \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (zAccelerationHi zAccelerationLo). \ \ ****************************************************************************** .zAccelerationHi EQUB 0 \ ****************************************************************************** \ \ Name: spinYawDeltaHi \ Type: Variable \ Category: Driving model \ Summary: High byte of the change in the spin yaw angle for the player's car \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (spinYawDeltaHi spinYawDeltaLo). \ \ ****************************************************************************** .spinYawDeltaHi EQUB 0 \ ****************************************************************************** \ \ Name: xPlayerAccelHi \ Type: Variable \ Category: Driving model \ Summary: High byte of the x-coordinate of the change in velocity of the \ player's car in the player's frame of reference \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (xPlayerAccelHi xPlayerAccelLo). \ \ ****************************************************************************** .xPlayerAccelHi EQUB 0 \ ****************************************************************************** \ \ Name: zPlayerAccelHi \ Type: Variable \ Category: Driving model \ Summary: High byte of the z-coordinate of the change in velocity of the \ player's car in the player's frame of reference \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (zPlayerAccelHi zPlayerAccelLo). \ \ ****************************************************************************** .zPlayerAccelHi EQUB 0 \ ****************************************************************************** \ \ Name: xVelocityHi \ Type: Variable \ Category: Driving model \ Summary: High byte of the x-coordinate of the player's velocity vector, \ from the player's frame of reference \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (xVelocityHi xVelocityLo). \ \ ****************************************************************************** .xVelocityHi EQUB 0 \ ****************************************************************************** \ \ Name: zVelocityHi \ Type: Variable \ Category: Driving model \ Summary: High byte of the z-coordinate of the player's velocity vector, \ from the player's frame of reference \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (zVelocityHi zVelocityLo). \ \ ****************************************************************************** .zVelocityHi EQUB 0 \ ****************************************************************************** \ \ Name: xTyreForceNoseHi \ Type: Variable \ Category: Driving model \ Summary: High byte of the x-coordinate of the force vector produced by the \ front tyres in the nose of the car \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (xTyreForceNoseHi xTyreForceNoseLo). \ \ ****************************************************************************** .xTyreForceNoseHi EQUB 0 \ ****************************************************************************** \ \ Name: xTyreForceRearHi \ Type: Variable \ Category: Driving model \ Summary: High byte of the x-coordinate of the force vector produced by the \ rear tyres of the car \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (xTyreForceRearHi xTyreForceRearLo). \ \ ****************************************************************************** .xTyreForceRearHi EQUB 0 \ ****************************************************************************** \ \ Name: zTyreForceNoseHi \ Type: Variable \ Category: Driving model \ Summary: High byte of the z-coordinate of the force vector produced by the \ front tyres in the nose of the car \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (zTyreForceNoseHi zTyreForceNoseLo). \ \ ****************************************************************************** .zTyreForceNoseHi EQUB 0 \ ****************************************************************************** \ \ Name: zTyreForceRearHi \ Type: Variable \ Category: Driving model \ Summary: High byte of the z-coordinate of the force vector produced by the \ rear tyres \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (zTyreForceRearHi zTyreForceRearLo). \ \ ****************************************************************************** .zTyreForceRearHi EQUB 0 \ ****************************************************************************** \ \ Name: xSteeringForceHi \ Type: Variable \ Category: Driving model \ Summary: High byte of the sideways force applied by steering \ \ ------------------------------------------------------------------------------ \ \ Stored as a 16-bit value (xSteeringForceHi xSteeringForceLo). \ \ ****************************************************************************** .xSteeringForceHi EQUB 0 \ ****************************************************************************** \ \ Name: firstLapStarted \ Type: Variable \ Category: Drivers \ Summary: Flag to keep track of whether we have started the first lap of \ practice or qualifying \ \ ------------------------------------------------------------------------------ \ \ For practice and qualifying laps, firstLapStarted keeps track of whether we \ have started the first lap (at which point the lap timer starts). \ \ Before we reach the starting line, firstLapStarted is -33, which is the value \ it gets in ResetVariables for practice or qualifying laps. This is then \ incremented to 0 when we start the first lap. \ \ The value of firstLapStarted is decremented with each call to UpdateLapTimers \ for practice or qualifying, but only if bit 6 of updateDrivingInfo is set and \ firstLapStarted is non-zero. I am not sure why. \ \ ****************************************************************************** .firstLapStarted EQUB 0 \ ****************************************************************************** \ \ Name: liftFromTorque \ Type: Variable \ Category: Driving model \ Summary: The lift of the front of the car caused by acceleration or braking \ \ ****************************************************************************** .liftFromTorque EQUB 0 \ ****************************************************************************** \ \ Name: wingBalance \ Type: Variable \ Category: Driving model \ Summary: Value used in the driving model when calculating the effect of the \ two wing settings \ \ ------------------------------------------------------------------------------ \ \ The wing balance is calculated as: \ \ 60 + (rearWingSetting * 3 + frontWingSetting) / 2 \ \ ****************************************************************************** .wingBalance EQUB 0 \ ****************************************************************************** \ \ Name: playerSideways \ Type: Variable \ Category: Car geometry \ Summary: Contains the player's heading, for testing whether the car is \ pointing sideways \ \ ------------------------------------------------------------------------------ \ \ If playerSideways < 40, then the player's car is facing forwards or backwards \ along the track. \ \ If playerSideways >= 40, then the player's car is facing sideways, relative to \ the direction of the track. \ \ ****************************************************************************** .playerSideways EQUB 0 \ ****************************************************************************** \ \ Name: thisObjectType \ Type: Variable \ Category: 3D objects \ Summary: The type of object we are currently drawing \ \ ****************************************************************************** .thisObjectType EQUB 0 \ ****************************************************************************** \ \ Name: thisDriverNumber \ Type: Variable \ Category: Drawing objects \ Summary: The number of the current driver when drawing cars \ \ ****************************************************************************** .thisDriverNumber EQUB 0 \ ****************************************************************************** \ \ Name: newSectionFetched \ Type: Variable \ Category: Track geometry \ Summary: Flag that determines whether a new track section has been fetched \ \ ****************************************************************************** .newSectionFetched EQUB 0 \ Track section status: \ \ * 0 = no new section \ \ * Non-zero = the player's car has entered a new \ track section \ ****************************************************************************** \ \ Name: crashedIntoFence \ Type: Variable \ Category: Car geometry \ Summary: Flag that records whether we have crashed into the fence \ \ ****************************************************************************** .crashedIntoFence EQUB 0 \ Crash status \ \ * 0 = we have not crashed into the fence \ \ * &FF = we have crashed into the fence \ ****************************************************************************** \ \ Name: irqCounter \ Type: Variable \ Category: Screen mode \ Summary: Counter that gets incremented every time the IRQ routine reaches \ section 4 of the custom screen \ \ ****************************************************************************** .irqCounter EQUB 0 \ ****************************************************************************** \ \ Name: updateLapTimes \ Type: Variable \ Category: Drivers \ Summary: Controls whether moving the car forward updates the lap number and \ lap times \ \ ****************************************************************************** .updateLapTimes EQUB 0 \ ****************************************************************************** \ \ Name: previousSignNumber \ Type: Variable \ Category: Track geometry \ Summary: Stores the number of the sign from the previous call to the \ BuildRoadSign routine \ \ ------------------------------------------------------------------------------ \ \ Set to 23 in ResetVariables. \ \ ****************************************************************************** .previousSignNumber EQUB 0 \ ****************************************************************************** \ \ Name: tyreTravel \ Type: Variable \ Category: Dashboard \ Summary: Keeps track of how far we have travelled so we know when to \ animate the tyres \ \ ****************************************************************************** .tyreTravel EQUB 0 \ ****************************************************************************** \ \ Name: playerDrift \ Type: Variable \ Category: Driving model \ Summary: Records whether the player's car is moving sideways by a \ significant amount \ \ ****************************************************************************** .playerDrift EQUB 0 \ If bit 7 is set, then the player's car is moving \ sideways by more than 22 in this iteration around the \ main driving loop (where the track width is 256) \ ****************************************************************************** \ \ Name: horizonTrackWidth \ Type: Variable \ Category: Track geometry \ Summary: The arc of the track at the horizon (i.e. the track width at the \ horizon), halved \ \ ****************************************************************************** .horizonTrackWidth EQUB 0 \ ****************************************************************************** \ \ Name: lowestTrackLine \ Type: Variable \ Category: Screen buffer \ Summary: Used to prevent objects from being drawn below the horizon line \ \ ****************************************************************************** .lowestTrackLine EQUB 0 \ ****************************************************************************** \ \ Name: updateDriverInfo \ Type: Variable \ Category: Text \ Summary: Flag that controls whether the driver names are updated in the \ information block at the top of the screen \ \ ------------------------------------------------------------------------------ \ \ If bit 7 is set, then we update the driver names in the "In front" and \ "Behind" sections at the top of the screen. \ \ Bit 7 gets set in ResetVariables, for race laps only. \ \ ****************************************************************************** .updateDriverInfo EQUB 0 \ ****************************************************************************** \ \ Name: zTyreForceBoth \ Type: Variable \ Category: Driving model \ Summary: High byte of the z-coordinate of the force vector produced by the \ front and tyres combined \ \ ****************************************************************************** .zTyreForceBoth EQUB 0 \ ****************************************************************************** \ \ Name: GetTextInput \ Type: Subroutine \ Category: Keyboard \ Summary: Fetch a string from the keyboard, padded with spaces if required \ \ ------------------------------------------------------------------------------ \ \ This routine fetches a string of characters from the keyboard and stores the \ result in memory. The string is entered by pressing RETURN, at which point the \ string in memory is padded with spaces so that it meets the required length. \ \ The DELETE key is supported, leading spaces are ignored, and the ESCAPE key is \ trapped and has no effect. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ (Y A) The address where the string should be stored \ \ X The length of string that we require \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ Y The number of characters entered, before any padding is \ applied \ \ ****************************************************************************** .GetTextInput STA P \ Set (Q P) = (Y A) STY Q STX W \ Store the required length of input in W LDA #2 \ Call OSBYTE with A = 2 and X = 0 to select the LDX #0 \ keyboard as the input stream and disable RS423 JSR OSBYTE LDA #21 \ Call OSBYTE with A = 21 and X = 0 to flush the LDX #0 \ keyboard buffer JSR OSBYTE .text1 LDY #0 \ Set Y = 0 to use as a counter for the number of \ characters entered .text2 JSR OSRDCH \ Call OSRDCH to read a character from the currently \ selected input stream (i.e. the keyboard) into A BCS text7 \ If the call to OSRDCH set the C flag then there was an \ error (probably caused by pressing ESCAPE), so jump to \ text7 to process this CMP #13 \ If RETURN was pressed, jump to text9 BEQ text9 CMP #' ' \ If a control character was entered (i.e. with an ASCII BCC text2 \ code less than that of " "), jump back to text2 to \ ignore it and wait for another key press BNE text3 \ If a key other than SPACE was pressed, jump to text3 \ skip the next two instructions CPY #0 \ SPACE was pressed, so if no other characters have been BEQ text2 \ (i.e. Y = 0), jump back to text2 to ignore it .text3 CMP #127 \ If the character entered has an ASCII value < 127, BCC text4 \ jump to text4 to process it as a valid character BNE text2 \ If the character entered has an ASCII value > 127, \ jump back to text2 to ignore it \ If we get here then the DELETE key was pressed, which \ has an ASCII value of 127 DEY \ Decrement the number of characters entered in Y, to \ process the deletion BPL text6 \ If Y is still positive, jump to text6 to print the \ delete character, which will delete the last character \ entered on-screen BMI text1 \ Y is negative, so we just deleted past the start of \ the entered string, so jump to text1 to set Y to 0 and \ start again (this BMI is effectively a JMP as we just \ passed through a BPL) .text4 \ If we get here then a valid character was entered CPY W \ If the number of characters entered in Y is not yet BNE text5 \ the required number in W, jump to text5 to store the \ new character LDA #7 \ Otherwise set A = 7 (the ASCII code for a beep) and BNE text6 \ jump to text6 to skip storing the new character and \ make a beep, as we already have enough characters .text5 \ If we get here then we have successfully fetched a new \ character, so now we store it STA (P),Y \ Store the character entered in the Y-th byte of (Q P) INY \ Increment the character counter in Y .text6 JSR OSWRCH \ Print the character in A, which will either be the new \ character, or a beep, or a delete JMP text2 \ Jump up to text2 to fetch the next character .text7 \ If we get here then ESCAPE was pressed TYA \ Store the character count in Y on the stack PHA LDA #126 \ Call OSBYTE with A = 126 to acknowledge the ESCAPE JSR OSBYTE \ condition PLA \ Retrieve the character count from the stack into Y TAY JMP text2 \ Jump up to text2 to fetch the next character .text8 INY \ We get here from below after appending a space to the \ stored string, so we increment Y and repeat the \ padding process until the string is full .text9 \ If we get here then RETURN was pressed CPY W \ If the number of characters entered in Y is not yet BNE text10 \ the required number in W, jump to text10 to pad out \ the string with spaces RTS \ Otherwise the string is the correct size, so we now \ return from the subroutine .text10 LDA #' ' \ Append a space to the end of the stored string STA (P),Y BNE text8 \ Jump back to text8 to keep padding the string with \ spaces (this BNE is effectively a JMP as A is never \ zero) \ ****************************************************************************** \ \ Name: SetDriverSpeed \ Type: Subroutine \ Category: Drivers \ Summary: Set the speed for a specific driver \ Deep dive: Tactics of the non-player drivers \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ setSpeedForDriver The number of the driver to set the speed for \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ X The number of the previous driver \ \ setSpeedForDriver Points to the next driver, ready for the next call to \ SetDriverSpeed \ \ ****************************************************************************** .SetDriverSpeed LDX setSpeedForDriver \ Set X to the driver number to initialise \ We now start a lengthy calculation of a figure in A \ that we will add to the track's base speed to \ determine the speed for this driver, with a higher \ figure in A giving the car a higher speed in the race 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 PHP \ Store the processor flags from the random timer value \ on the stack AND #%01111111 \ Clear bit 0 of the random number in A, so A is now in \ the range 0 to 127 \ The following calculates the following: \ \ * If A is in the range 0 to 63, A = A mod 4 \ \ * If A is in the range 64 to 127, A = (A - 64) mod 7 \ \ Given that A starts out as a random number, this will \ produce a random number with the following chances: \ \ 50% of the time, 25% chance of 0 to 3 \ 50% of the time, 12.5% chance of 0 to 7 \ \ So the probability of getting each of the numbers from \ 0 to 7 is: \ \ 0 to 3 = 0.5 * 0.25 + 0.5 * 0.125 = 0.1875 = 18.75% \ 4 to 7 = 0.5 * 0.125 = 0.0625 = 6.25% \ \ So we're three times more likely to get a number in \ the range 0 to 3 as in the range 4 to 7 LDY #16 \ Set a loop counter in Y to subtract 16 lots of 4 .fast1 CMP #4 \ If A < 4, jump to fast3, as A now contains the BCC fast3 \ original value of A mod 4 SBC #4 \ A >= 4, so set A = A - 4 DEY \ Decrement the loop counter BNE fast1 \ Loop back until we have either reduced A to be less \ than 4, or we have subtracted 16 * 4 = 64 \ If we get here then the original A was 64 or more, \ 64, and A is now in the range 0 to 63 LDY #9 \ Set a loop counter in Y to subtract 9 lots of 7 .fast2 CMP #7 \ If A < 7, jump to fast3, as A now contains the BCC fast3 \ original value of (A - 64) mod 7 SBC #7 \ A >= 7, so set A = A - 7 DEY \ Decrement the loop counter BNE fast2 \ Loop back until we have either reduced A to be less \ than 7, or we have subtracted 9 * 7 = 63 \ If we get here then the original A was 127, and we \ first subtracted 64 and then 63 to give us 0 .fast3 \ We now have our random number in the range 0 to 7, \ with 0 to 3 more likely than 4 to 7 PLP \ Retrieve the processor flags from the random timer \ value that we put on the stack above, which sets the \ N flag randomly (amongst others) JSR Absolute8Bit \ The first instruction of Absolute8Bit is a BPL, which \ normally skips negation for positive numbers, but in \ this case it means the Absolute8Bit routine randomly \ changes the sign of A, so A is now in the range \ -7 to +7, with -3 to +3 more likely than -7 to -4 or \ 4 to 7 ASL A \ Set A = A << 1, so A is now in the range -14 to +14, \ with -6 to +6 more likely than -14 to -7 or 7 to 14 SEC \ Set A = A - driverGridRow for this driver SBC driverGridRow,X \ \ So A is left alone for the two cars at the front of \ the grid, is reduced by 1 for the next two cars, and \ is reduced by 9 for the two cars at the back STA T \ Set T = A \ By this point, the value in A (and T) is in the range: \ \ * -14 to +14 for the front two cars \ * -15 to +13 for the next two cars \ ... \ * -23 to +4 for the last two cars \ \ We now alter this according to the race class LDY raceClass \ Set Y to the race class DEY \ Decrement Y, so it will be -1 for Novice, 0 for \ Amateur and 1 for Professional BEQ fast5 \ If Y = 0 (Amateur), jump to fast5 to leave A alone BPL fast4 \ If Y = 1 (Professional), jump to fast4 \ If we get here, then the race class is Novice ASL A \ Set A = A << 1 \ \ so A is now in the range: \ \ * -28 to +28 for the front two cars \ * -30 to +26 for the next two cars \ ... \ * -46 to +8 for the last two cars \ \ This makes the range of speeds less tightly bunched, \ so the race is less intense JMP fast5 \ Jump to fast5 to skip the following .fast4 \ If we get here, then the race class is Professional ROL T \ Shift bit 7 of T into the C flag, and because T = A, \ this puts bit 7 of A into the C flag ROR A \ Shift A right while inserting a copy of bit 7 into \ bit 7, so this effectively divides A by two while \ keeping the sign intact: \ \ A = A / 2 \ \ So A is now in the range: \ \ * -7 to +7 for the front two cars \ * -8 to +6 for the next two cars \ ... \ * -12 to +2 for the last two cars \ \ This makes the range of speeds more tightly bunched, \ so the race is more intense .fast5 \ By this point we have our value A, which determines \ the speed of the driver based on our random number, \ the car's grid position and the race class, so now we \ add this to the track's base speed to get the driver's \ speed for the race \ \ The track's base speed is different depending on the \ race class, so taking Silverstone as an example, we \ get these final ranges for the front two cars: \ \ * Novice = 134 plus -28 to +28 = 106 to 162 \ * Amateur = 146 plus -14 to +14 = 132 to 160 \ * Professional = 153 plus -7 to +7 = 146 to 160 \ \ and these final ranges for the two cars at the back: \ \ * Novice = 134 plus -46 to +8 = 88 to 142 \ * Amateur = 146 plus -23 to +4 = 123 to 150 \ * Professional = 153 plus -12 to +2 = 141 to 155 CLC \ Set the driverSpeed for driver X to baseSpeed + A ADC baseSpeed STA driverSpeed,X JSR GetPositionAhead \ Set X to the number of the position ahead of the \ driver whose speed we just set STX setSpeedForDriver \ Set setSpeedForDriver = X \ \ So the next call to the routine will set the speed for \ the next driver ahead RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: SetPlayerPositions \ Type: Subroutine \ Category: Drivers \ Summary: Set the current player's position, plus the positions behind and \ in front \ \ ****************************************************************************** .SetPlayerPositions LDA currentPlayer \ Set A to the number of the current player LDX #19 \ We are about to work our way through the ordered list \ of drivers in driversInOrder, so set a loop counter \ in X, starting at the end of the list (i.e. last \ place) .ppos1 CMP driversInOrder,X \ If the driver in position X in the list matches the BEQ ppos2 \ current player, jump to ppos2 DEX \ Decrement the driver number BPL ppos1 \ Loop back until we have gone through the whole table .ppos2 \ By this point, X contains the position within the \ driversInOrder list of the current player (or -1 if \ the current player doesn't appear in the list) STX currentPosition \ Store the current player's position in currentPosition JSR GetPositionBehind \ Set X to the number of the position behind this one STX positionBehind \ Store the position behind the current player in \ positionBehind LDX currentPosition \ Set X to the number of the position ahead of the JSR GetPositionAhead \ current player's position STX positionAhead \ Store the position ahead of the current player in \ positionAhead RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: Protect \ Type: Subroutine \ Category: Setup \ Summary: Decrypt or unprotect the game code (disabled) \ \ ****************************************************************************** IF _SUPERIOR OR _REVSPLUS .Protect JMP SetupGame \ Jump to SetupGame to continue setting up the game NOP \ Presumably this contained some kind of copy protection NOP \ or decryption code that has been replaced by NOPs in NOP \ this unprotected version of the game NOP NOP ENDIF \ ****************************************************************************** \ \ Name: GetSteeringAssist \ Type: Subroutine \ Category: Tactics \ Summary: Fetch the current computer assisted steering (CAS) status and show \ or hide the CAS indicator \ Deep dive: Computer assisted steering (CAS) \ \ ------------------------------------------------------------------------------ \ \ The computer assisted steering (CAS) indicator is in the centre-bottom of the \ rev counter, and is made up of four pixels in colour 2 (white) as follows: \ \ ...xx... \ ..x..x.. \ \ which would be encoded in mode 5 screen memory as follows: \ \ %00010000 %10000000 \ %00100000 %01000000 \ \ This routine shows or hides the indicator according to the current setting of \ configAssist, returning the value of configAssist in X. \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ X The current value of configAssist: \ \ * %10000000 if computer assisted steering is enabled \ \ * 0 if computer assisted steering is not enabled \ \ A A is unchanged \ \ C flag Set to bit 7 of directionFacing (clear if our car is \ facing forwards, set if we are facing backwards) \ \ ****************************************************************************** IF _SUPERIOR OR _REVSPLUS .GetSteeringAssist PHA \ Store A on the stack so we can retrieve it below LDA configAssist \ Set A to configAssist, which has the following value: \ \ * %10000000 if computer assisted steering is enabled \ \ * 0 if computer assisted steering is not enabled \ \ The following updates screen memory to add a small \ "hat" marker to the centre-bottom of the rev counter \ when CAS is enabled (or to remove the marker when it \ isn't enabled) STA assistRight1 \ Set assistRight1 = 0 or %10000000 LSR A \ Set assistRight2 = 0 or %01000000 STA assistRight2 LSR A \ Set assistLeft2 = 0 or %00100000 STA assistLeft2 LSR A \ Set assistLeft1 = 0 or %00010000 STA assistLeft1 LDA directionFacing \ Set the C flag to bit 7 of directionFacing, which we ROL A \ return from the subroutine PLA \ Restore the value of A that we put on the stack above LDX configAssist \ Set X to configAssist RTS \ Return from the subroutine ENDIF \ ****************************************************************************** \ \ Name: SetupGame \ Type: Subroutine \ Category: Setup \ Summary: Decrypt or unprotect the game code (disabled) \ \ ****************************************************************************** IF _SUPERIOR OR _REVSPLUS .SuperiorSetupGame CLEAR &3850, &3880 \ In the Superior Software release of Revs, the routines ORG &3850 \ for computer assisted steering (CAS) take up extra \ memory, so we need to claw back some memory from \ somewhere \ \ The clever solution is to move the SetupGame routine, \ which is run when the game loads, but is never needed \ again, so in the Superior version, SetupGame is put \ into the same block of memory as the carSpeedLo, \ totalPointsLo and totalPointsLo variables, which are \ only used after the game has started \ \ These lines rewind BeebAsm's assembly back to \ carSpeedLo (which is at address &3850), and clear the \ block that is occupied by these three variables, so we \ can assemble SetupGame in the right place while \ retaining the correct addresses for the three \ variables \ \ We also make a note of the current address, so we can \ ORG back to it after assembling SetupGame ENDIF .SetupGame LDA #4 \ Call OSBYTE with A = 4, X = 1 and Y = 0 to disable LDY #0 \ cursor editing LDX #1 JSR OSBYTE JSR SetScreenMode7 \ Change to screen mode 7 and hide the cursor LDX #9 \ We now zero the ten bytes starting at configStop, so \ set a loop counter in X LDA #0 \ Set lineBufferSize = 0, to reset the line buffer STA lineBufferSize .setp1 STA configStop,X \ Zero the X-th byte at configStop \ \ The address in this instruction gets modified DEX \ Decrement the loop counter BPL setp1 \ Loop back until we have zeroed all ten bytes LDA #246 \ Set volumeLevel = -10, which sets the sound level to STA volumeLevel \ medium (-15 is full volume, 0 is silent) TSX \ Store the stack pointer in startingStack so we can STX startingStack \ restore it when restarting the game JSR CallTrackHook \ Call the hook code in the track file (for Silverstone \ the hook routine is just an RTS, so this does nothing) IF _ACORNSOFT OR _4TRACKS \ Fall through into the main game loop to start the game ELIF _SUPERIOR OR _REVSPLUS LDA #190 \ Call OSBYTE with A = 190, X = %00100000 and Y = 0 to LDY #0 \ configure the digital joystick port on the BBC Master LDX #%00100000 \ Compact conversion type to 32-bit conversion (as the JSR OSBYTE \ Superior Software release was updated to work on this \ machine) \ \ The configuration does the following: \ \ * Bit 7 clear = update channel values from cursor \ keys and/or digital joystick \ \ * Bit 6 clear = do not simulate key presses from the \ digital joystick \ \ * Bit 5 set = return fixed values to channels 1 and \ 2 as follows: \ \ Left = &FFFF to channel 1 \ Centre (horizontal) = &7FFF to channel 1 \ Right = 0 to channel 1 \ Down = &FFFF to channel 2 \ Centre (vertical) = &7FFF to channel 2 \ Up = 0 to channel 2 \ \ * Bit 4 is unused \ \ * Bits 0-3 clear = emulate the analogue speed of \ joystick movement by returning slowly changing \ values related to the joystick movement JMP MainLoop \ Jump to the main game loop to start the game EQUB &FF \ This byte appears to be unused ORG SuperiorSetupGame \ Switch back to the original address, so we can \ continue with the assembly ENDIF \ ****************************************************************************** \ \ Name: MainLoop (Part 1 of 6) \ Type: Subroutine \ Category: Main loop \ Summary: The main game loop: practice laps \ Deep dive: Program flow of the main game loop \ \ ------------------------------------------------------------------------------ \ \ This part of the main loop implements practice laps. \ \ ****************************************************************************** .MainLoop LDX #0 \ Set configStop = 0 so we clear out any existing STX configStop \ stop-related key presses JSR InitialiseDrivers \ Initialise all 20 drivers LDX #4 \ Print "REVS REVS REVS" as a double-height header JSR PrintHeader \ at column 0, row 4, with the colours of each letter in \ REVS set to magenta/yellow/cyan/green JSR PrintHeaderChecks \ Print chequered lines above and below the header LDX #39 \ Print token 39, which shows a menu with the following JSR PrintToken \ options: \ \ 1 = PRACTICE \ \ 2 = COMPETITION LDX #2 \ Fetch the menu choice into X (0 to 1) JSR GetMenuOption CPX #1 \ If X >= 1, then the choice was competition, so jump to BCS game1 \ game1 to start setting up the competition races STX currentPlayer \ Otherwise X = 0 and the choice was practice, so set \ currentPlayer = 0 DEX \ Set qualifyingTime = 255, so that the time we spend STX qualifyingTime \ practicing is as long as we want JSR ResetBestLapTimes \ Reset the best lap times to 10:00.0 for all drivers JSR HeadToTrack \ Head to the track to choose the wing settings and \ start the practice laps, which the player exits by \ pressing SHIFT and right arrow to restart the game \ (so we don't return from this call) \ ****************************************************************************** \ \ Name: MainLoop (Part 2 of 6) \ Type: Subroutine \ Category: Main loop \ Summary: The main game loop: competition setup \ Deep dive: Program flow of the main game loop \ \ ------------------------------------------------------------------------------ \ \ This part of the main loop gets all the general information required for the \ competition: the race class and the duration of qualifying laps. \ \ ****************************************************************************** .game1 LDA #0 \ Set competitionStarted = 0, to indicate that the STA competitionStarted \ competition hasn't started yet (so we still need to \ get the race class, the number of laps, and the \ players' names) LDX #21 \ Print token 21, which shows a menu with the following JSR PrintToken \ options: \ \ Prompt = SELECT THE CLASS OF RACE \ \ 1 = Novice \ \ 2 = Amateur \ \ 3 = Professional LDX #3 \ Fetch the menu choice into X (0 to 2) JSR GetMenuOption STX raceClass \ Set raceClass to the chosen race class (0 to 2) JSR GetSectionSteering \ Set up the optimum steering for each section for this \ race class, storing the results in sectionSteering .game2 LDX #22 \ Print token 22, which shows a menu with the following JSR PrintToken \ options: \ \ Prompt = SELECT DURATION OF QUALIFYING LAPS \ \ 1 = 5 mins \ \ 2 = 10 mins \ \ 3 = 20 mins LDX #3 \ Fetch the menu choice into X (0 to 2) JSR GetMenuOption LDA timeFromOption,X \ Set the value of qualifyingTime to 4, 9 or 25, which STA qualifyingTime \ should be the number of minutes of qualifying time \ minus one, but the latter seems to be a bug \ ****************************************************************************** \ \ Name: MainLoop (Part 3 of 6) \ Type: Subroutine \ Category: Main loop \ Summary: The main game loop: qualifying laps \ Deep dive: Program flow of the main game loop \ \ ------------------------------------------------------------------------------ \ \ This part of the main loop gets the players' driver names, and heads to the \ track for their qualifying laps. \ \ ****************************************************************************** JSR ResetBestLapTimes \ Reset the best lap times to 10:00.0 for all drivers LDA #20 \ Set currentPlayer = 20, so the first human player will STA currentPlayer \ be number 19, the next one will be 18, and so on .game3 DEC currentPlayer \ Decrement currentPlayer so it points to the next human \ player LDX currentPlayer \ Set X to the new player number JSR ResetBestLapTime \ Reset the best lap time to 10:00.0 for the new player LDA competitionStarted \ If competitionStarted = 0, jump to game4 to ask for BEQ game4 \ the player's name, as the competition hasn't started \ yet and we are still running qualifying laps \ If we get here then the competition is in full swing, \ so we need to run qualifying laps for all the human \ players JSR PrintDriverPrompt \ Print the "DRIVER ->" prompt and the player's name JSR HeadToTrack \ Head to the track to choose the wing settings and \ drive the qualifying laps, returning here when the \ laps are finished LDA currentPlayer \ If currentPlayer <> lowestPlayerNumber, then we still CMP lowestPlayerNumber \ have more qualifying laps to get, so jump back to BNE game3 \ game3 for the next player's qualifying laps \ By this point we have all the qualifying times for the \ human players LDA #0 \ Sort the drivers by lap time, putting the results into JSR SortDrivers \ positionNumber and driversInOrder JMP game9 \ Jump down to game9 to print the driver table, showing \ the grid positions for the race .game4 \ If we get here then we need to get the player's name, \ as the competition has not yet started LDX #23 \ Print token 23, which shows the following prompt: JSR PrintToken \ \ ENTER NAME OF DRIVER \ \ and a text prompt: \ \ > \ ------------ JSR GetDriverName \ Fetch the player's name from the keyboard JSR HeadToTrack \ Head to the track to choose the wing settings and \ start the qualifying laps, returning here when the \ laps are finished LDX currentPlayer \ If currentPlayer = 0 then we have got as many players BEQ game5 \ as we can handle (20 of them), so jump to game5 to \ skip asking for another driver LDX #27 \ Print token 27, which shows a menu with the following JSR PrintToken \ options: \ \ 1 = ENTER ANOTHER DRIVER \ \ 2 = START RACE LDX #2 \ Fetch the menu choice into X (0 to 1) JSR GetMenuOption CPX #0 \ If X = 0, then the choice was to enter another driver, BEQ game3 \ so jump back to game3 to add the new player and head \ to the track for their qualifying laps \ ****************************************************************************** \ \ Name: MainLoop (Part 4 of 6) \ Type: Subroutine \ Category: Main loop \ Summary: The main game loop: the competition race \ Deep dive: Program flow of the main game loop \ \ ------------------------------------------------------------------------------ \ \ This part of the main loop checks the slowest qualifying lap times to see if \ we should make the game easier. \ \ ****************************************************************************** .game5 LDA currentPlayer \ We have now got all the player names and qualifying STA lowestPlayerNumber \ times that we need, so store the player number in \ lowestPlayerNumber, which will contain 19 if there is \ one player, 18 if there are two, and so on \ \ Human players take the place of drivers with higher \ numbers, so the first player takes the place of driver \ 19 (the aptly called Dummy Driver, as they never get \ to race), and the second player takes the place of \ driver 18 (Peter Out), the third player replaces \ driver 17 (Rick Shaw) and so on \ \ So this not only represents the lowest player number, \ but also the highest non-human driver number (which is \ lowestPlayerNumber - 1) LDA #0 \ Sort the drivers by best lap time, putting the results JSR SortDrivers \ into positionNumber and driversInOrder, so they now \ contain the order of the drivers on the grid for the \ coming race \ We now adjust the class of the race to cater for the \ player's qualifying lap time, which makes things more \ fun for a mixed group of player skills LDX #0 \ Set X to the race class number for Novice .game6 LDY driversInOrder+19 \ Set Y to the driver number with the slowest lap time CPY lowestPlayerNumber \ If Y < lowestPlayerNumber, then driver Y is one of the BCC game8 \ computer-controlled drivers and they have the slowest \ lap time, so jump to game8 as the race class doesn't \ need changing \ Otherwise the slowest lap time is by one of the human \ players, so we now set the race class (i.e. the race \ difficulty setting) according to the figures in the \ track data) LDA bestLapSeconds,Y \ Calculate the slowest lap time minus the time for SEC \ class X from the track data, starting with the seconds SBC trackLapTimeSec,X LDA bestLapMinutes,Y \ And then the minutes SBC trackLapTimeMin,X \ Note that for X = 2 (professional), the track data \ figure is 0, so the C flag will always be set BCS game7 \ If the slowest lap time is longer than trackLapTimeMin \ from the track data, jump to game7 to consider setting \ the class to X, if it is easier than the current class \ \ In other words, if the slowest lap time is really slow \ and is by a human player, this can make the game \ easier if the race class isn't already on Novice \ \ For example, for Silverstone: \ \ * We will consider setting the class to Novice if \ the longest lap is > 1:33 and by a human player \ \ * We will consider setting the class to Amateur if \ the longest lap is <= 1:33 and > 1:29 and by a \ human player \ \ * We will consider setting the class to Professional \ if the longest lap is <= 1:29 and by a human \ player \ If we get here, the slowest lap time is quicker than \ the figure from the track data, so we now check the \ next figure from the track data and try again INX \ Increment X to the next difficulty level BNE game6 \ Jump back to game6 to check the next race class (this \ BNE is effectively a JMP as X is never zero) .game7 CPX raceClass \ If X >= raceClass then X is the same or more difficult BCS game8 \ than the current setting, so jump to game8 to leave \ the class unchanged STX raceClass \ Otherwise set the race class to the easier class in X \ ****************************************************************************** \ \ Name: MainLoop (Part 5 of 6) \ Type: Subroutine \ Category: Main loop \ Summary: The main game loop: the competition race \ Deep dive: Program flow of the main game loop \ \ ------------------------------------------------------------------------------ \ \ This part of the main loop heads to the track to run the actual race. We \ print the driver table showing the grid positions, and set the grid row for \ any human drivers. We then start a loop, running a race for each human player, \ and asking for the number of laps in the race for the first such race. The \ loop concludes in the next part of the main loop. \ \ ****************************************************************************** .game8 LDX raceClass \ Set X to the race class JSR GetSectionSteering \ Set up the optimum steering for each section for this \ race class, storing the results in sectionSteering LDX #26 \ Print token 26, which is a double-height header with JSR PrintToken \ the text "STANDARD OF RACE" JSR PrintRaceClass \ Print the race class JSR WaitForSpace \ Print a prompt and wait for SPACE to be pressed .game9 LDX #2 \ Print the driver table, showing lap times, under the LDA #0 \ heading "GRID POSITIONS", so this shows the drivers JSR PrintDriverTable \ in their starting positions on the grid \ We now make a copy of the driversInOrder list into \ driversInOrder2, so we can retrieve it below, and at \ the same time we set the correct grid row for any \ human players, depending on their starting position \ on the grid (there are two cars per grid row) LDY #19 \ Set up a counter in Y so we can work through the \ drivers in order, from the back of the grid to the \ front .game10 LDA driversInOrder,Y \ Copy the Y-th position from driversInOrder to STA driversInOrder2,Y \ driversInOrder2, setting A to the number of the driver \ in position Y CMP lowestPlayerNumber \ If A < lowestPlayerNumber, then driver A is one of the BCC game11 \ computer-controlled drivers, so jump to game11 to skip \ setting the grid number for the driver \ If we get here then driver A is a human player TAX \ Set X = the player's driver number LDA positionNumber,Y \ Set A = the position of the player on the grid LSR A \ Set the driver's grid row to A / 2, so the first two STA driverGridRow,X \ drivers are on grid row 0, then the next two are on \ grid row 1, and so on .game11 DEY \ Decrement the counter BPL game10 \ Loop back until we have saved all the positions LDA competitionStarted \ If competitionStarted <> 0, then the competition has BNE game12 \ already started, so jump to game12 to skip the lap \ selection process LDX #28 \ Print token 28, which shows a menu with the following JSR PrintToken \ options: \ \ Prompt = SELECT NUMBER OF LAPS \ \ 1 = 5 laps \ \ 2 = 10 laps \ \ 3 = 20 laps LDA #20 \ Set numberOfPlayers = 20 - lowestPlayerNumber SEC \ SBC lowestPlayerNumber \ so numberOfPlayers is 1 if there is one player, 2 if STA numberOfPlayers \ there are two players, and so on LDX #3 \ Fetch the menu choice into X (0 to 2) JSR GetMenuOption LDA lapsFromOption,X \ Convert the menu choice (0 to 2) into the number of STA numberOfLaps \ laps (5, 10, 20) using the lapsFromOption lookup, and \ store the result in numberOfLaps STX lapsMenuOption \ Store the menu choice (0 to 2) in lapsMenuOption .game12 LDA #20 \ We now work our way through the human players so each STA currentPlayer \ one can race in turn, so set currentPlayer to 20 so \ it gets decremented to 19 for the first player .game13 DEC currentPlayer \ Decrement currentPlayer to move on to the next player JSR PrintDriverPrompt \ Print the "DRIVER ->" prompt and the player's name LDX #19 \ We now restore the grid positions we saved above, so \ set a counter in X .game14 LDA driversInOrder2,X \ Restore the X-th position from driversInOrder2 to STA driversInOrder,X \ driversInOrder DEX \ Decrement the counter BPL game14 \ Loop back until we have restored all the positions JSR ResetBestLapTimes \ Reset the best lap times to 10:00.0 for all drivers LDA #%10000000 \ Head to the track to choose the wing settings and JSR HeadToTrack+2 \ start the race (as bit 7 of A is set), returning here \ when the race is finished \ ****************************************************************************** \ \ Name: MainLoop (Part 6 of 6) \ Type: Subroutine \ Category: Main loop \ Summary: The main game loop: race points and competition results \ Deep dive: Program flow of the main game loop \ \ ------------------------------------------------------------------------------ \ \ This part of the main loop processes the points from the race and displays the \ driver tables, and loops back to the previous part if there are more human \ players in the race. When all the players have raced, we loop back to part 2 \ for the next race in the championship. \ \ ****************************************************************************** LDA #%10000000 \ Sort the drivers by total race time, putting the JSR SortDrivers \ results into positionNumber and driversInOrder LDX #5 \ We now award points to the top six drivers in the \ race, so set a counter in X .game15 JSR AwardRacePoints \ Award points to the driver in race position X DEX \ Decrement the counter BPL game15 \ Loop back until we have awarded points to the top six \ drivers LDA #0 \ Sort the drivers by best lap time, putting the results JSR SortDrivers \ into positionNumber and driversInOrder LDX #6 \ Award a point to the driver with the fastest lap JSR AwardRacePoints .game16 LDA #%10000000 \ Sort the drivers by total race time, putting the JSR SortDrivers \ results into positionNumber and driversInOrder LDX #1 \ Print the driver table, showing points awarded in the LDA #4 \ last race, under the heading "POINTS", so this shows JSR PrintDriverTable \ the best drivers from the last race, along with the \ points awarded to the fastest six drivers LDA #0 \ Sort the drivers by best lap time, putting the results JSR SortDrivers \ into positionNumber and driversInOrder LDX #6 \ Print the driver table, showing lap times, under the LDA #0 \ heading "BEST LAP TIMES", so this shows the lap times JSR PrintDriverTable \ from the race LDA #%01000000 \ Sort the drivers by accumulated points, putting the JSR SortDrivers \ results into positionNumber and driversInOrder LDX #3 \ Set competitionStarted = 3, which is non-zero, so this STX competitionStarted \ indicates that the competition has started (so we \ don't get asked to choose the number of laps or player \ names) LDA #&88 \ Print the driver table, showing accumulated points, JSR PrintDriverTable \ under the heading "ACCUMULATED POINTS", so this shows \ the cumulative results of all races BIT G \ If bit 7 of G is clear, then RETURN was pressed, so BPL game16 \ jump back to game16 to show the driver tables again LDA currentPlayer \ If currentPlayer <> lowestPlayerNumber, then we still CMP lowestPlayerNumber \ have more players to race, so jump back to game13 to BNE game13 \ start the next player's race JMP game2 \ Jump back to game2 to move on to the next race in the \ competition \ ****************************************************************************** \ \ Name: HeadToTrack \ Type: Subroutine \ Category: Main Loop \ Summary: Get the wing settings and start a race, practice or qualifying lap \ Deep dive: Program flow of the main game loop \ \ ------------------------------------------------------------------------------ \ \ Other entry points: \ \ HeadToTrack+2 Called with A = %10000000 to start a race, as opposed to \ practice or a qualifying lap \ \ ****************************************************************************** .HeadToTrack LDA #%00101000 \ Set A to the value for practice or a qualifying lap \ (this instruction will be skipped when starting a race \ by calling HeadToTrack+2) STA raceStarted \ Set raceStarted to the value of A, so bit 7 gets set \ if this is a race, but is clear for practice or a \ qualifying lap STA raceStarting \ Set raceStarting to the value of A, which will be 128 \ if this is a race, so the starting lights get shown \ and the restrictions of the starting grid are applied \ (i.e. can't move the car, timers are disabled) \ \ If this not a race then we clear bit 7 of raceStarting \ so the restrictions are not applied .race1 JSR GetWingSettings \ Get the front and rear wing settings from the player JSR MainDrivingLoop \ Call the main driving loop to switch to the custom \ mode, implement the driving part of the game, and \ return here with the screen mode back to mode 7 BIT configStop \ If bit 6 of configStop is set then we are returning to BVS race1 \ the track after visiting the pits, so loop back to \ race1 to get new wing settings before rejoining the \ driving loop BPL race2 \ If bit 7 of configStop is clear then we did not use \ SHIFT and right arrow to exit the main driving loop, \ so jump to race2 to return from the subroutine \ If we get here then bit 6 of configStop is clear and \ bit 7 of configStop is set, which means we presses \ SHIFT and right arrow to exit the main driving loop, \ which is the key combination for restarting the whole \ game JSR RestartGame \ Jump to RestartGame to restart the game, which removes \ the return address from the stack and jumps to the \ main loop, so this call acts like JMP RestartGame .race2 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: GetMenuOption \ Type: Subroutine \ Category: Keyboard \ Summary: Scan the keyboard for a menu entry number, highlight the choice, \ show the SPACE bar message and return the choice number \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The number of entries in the menu \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ X The chosen option, zero-based (so the first option is 0, \ the second option is 1, and so on) \ \ ****************************************************************************** .GetMenuOption LDY #0 \ Set W = 0, to indicate that we have not yet chosen an STY W \ option from the menu STX U \ Store the number of entries in U .mopt1 JSR CheckRestartKeys \ Check whether the restart keys are being pressed, and \ if they are, restart the game (the restart keys are \ SHIFT and right arrow) LDY U \ We now loop through each valid menu option for this \ menu and check whether the relevant key is being \ pressed, so we set a loop counter in U to start from \ the menu size and loop down to zero .mopt2 STY V \ Store the loop counter in V so we can retrieve it \ below IF _ACORNSOFT OR _4TRACKS LDX menuKeys,Y \ Fetch the key number for menu option Y ELIF _SUPERIOR OR _REVSPLUS LDX menuKeysSup,Y \ Fetch the key number for menu option Y ENDIF JSR ScanKeyboard \ Scan the keyboard to see if this key is being pressed BEQ mopt3 \ If this key is being pressed, jump to mopt3 LDY V \ Retrieve the value of the loop counter DEY \ Decrement the loop counter to scan for the next menu \ option BPL mopt2 \ Loop back to check the key for the next option BMI mopt1 \ Loop back to mopt1 to keep checking through the option \ keys (this BMI is effectively a JMP as we just passed \ through a BPL) .mopt3 LDY V \ Set Y to the menu option that was chosen BNE mopt4 \ If Y is non-zero, jump to mopt4 to process the choice \ If we get here, SPACE was pressed LDA W \ If W = 0 then we have not yet chosen an option from BEQ mopt1 \ the menu, so jump back to mopt1 to keep checking for \ key presses, as SPACE is only a valid key press when \ we have chosen an option \ If we get here then we have already chosen an option \ from the menu, and SPACE has been pressed LDA #152 \ Poke the mode 7 conceal character into screen memory, STA row24_column5 \ to hide row 24 from column 5 onwards, i.e. hide the \ "PRESS SPACE BAR TO CONTINUE" prompt LDX G \ Set X = G - 1, to return as the zero-based choice DEX \ number RTS \ Return from the subroutine .mopt4 STY G \ Set G to the number of the choice we just made LDA W \ If W is non-zero, jump to mopt5 to skip the following BNE mopt5 \ three instructions LDX #30 \ Set X = 30 to pass to PrintToken below STX W \ Set W = 30, so W is now non-zero and denotes that we \ have made a choice JSR PrintToken \ Print token 30 ("PRESS SPACE BAR TO CONTINUE" in cyan \ at column 5, row 24) .mopt5 \ We now work our way through the menu, setting each \ entry's background colour according to the choice made \ (the chosen entry is set to red, while other entries \ are set to blue) LDX #0 \ Set an offset counter in X to step through the screen \ address of the on-screen number for each menu entry, \ starting at an offset of 0 (the offset is added to \ row18_column5 in the loop below) LDY #1 \ Set a counter in Y to count through the menu entries .mopt6 LDA #132 \ Set A to the mode 7 control code for blue, to set as \ the background colour for the unselected menu entries CPY G \ If Y <> G then this is not the chosen entry, so skip BNE mopt7 \ the following instruction to leave A as blue LDA #129 \ Set A to the mode 7 control code for red, to set as \ the background colour for the selected menu entry .mopt7 STA row18_column5,X \ Poke the colour in A into screen memory at offset X \ from column 5 on row 18, which is the screen address \ of the number for the first menu entry (so this sets \ the background colour of the current entry to A) TXA \ Set X = X + 80 CLC \ ADC #80 \ so X now points to the next menu entry, as 80 is two TAX \ lines of mode 7 characters, and the menu entries are \ shown on every other line INY \ Increment the option counter in Y CPY U \ If Y <= U then loop back to set the background colour BCC mopt6 \ for the next option, until we have done all of them BEQ mopt6 BNE mopt1 \ Jump back to mopt1 to keep checking for key presses, \ so we can change the option, or press SPACE to lock in \ our choice (this BNE is effectively a JMP as we just \ passed through a BEQ) \ ****************************************************************************** \ \ Name: ConvertNumberToBCD \ Type: Subroutine \ Category: Maths (Arithmetic) \ Summary: Convert a number into binary coded decimal (BCD), for printing \ \ ------------------------------------------------------------------------------ \ \ This routine converts a number in the range 0 to 19 into a BCD number in the \ range 1 to 20, so the number can be printed. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ A The number to be converted into BCD (0 to 19) \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ A The number in BCD (1 to 20) \ \ ****************************************************************************** .ConvertNumberToBCD CMP #10 \ If A < 10, skip the following instruction as A is in BCC ibcd1 \ the range 0 to 9, which is the same number in BCD ADC #5 \ A >= 10, so set A = A + 6 (as the C flag is set) to \ convert the number into BCD, like this: \ \ * 10 = &0A -> &10 (i.e. 10 in BCD) \ * 11 = &0B -> &11 (i.e. 11 in BCD) \ * 12 = &0C -> &12 (i.e. 12 in BCD) \ * 13 = &0D -> &13 (i.e. 13 in BCD) \ * 14 = &0E -> &14 (i.e. 14 in BCD) \ * 15 = &0F -> &15 (i.e. 15 in BCD) \ * 16 = &10 -> &16 (i.e. 16 in BCD) \ * 17 = &11 -> &17 (i.e. 17 in BCD) \ * 18 = &12 -> &18 (i.e. 18 in BCD) \ * 19 = &13 -> &19 (i.e. 19 in BCD) .ibcd1 SED \ Set the D flag to switch arithmetic to Binary Coded \ Decimal (BCD) ADC #1 \ Increment A in BCD mode, so the result is in the \ range 1 to 20 CLD \ Clear the D flag to switch arithmetic to normal RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: PrintDriverTable \ Type: Subroutine \ Category: Text \ Summary: Print the table of drivers \ \ ------------------------------------------------------------------------------ \ \ The driver table consists of the following: \ \ * A header, as specified by the argument in X \ \ * A table with one row for each driver, showing a number, a driver name and \ a third column as specified by the argument in A \ \ * A "PRESS SPACE TO CONTINUE" prompt below the table \ \ If the table is being shown after practice or qualifying, the drivers are \ shown in driver order, from 1 to 20, but if it is shown after a race, the \ first column shows the numbers from the positionNumber table, the second \ column shows the drivers in the order that they appear in the driversInOrder \ list, and the race class is printed above the table. \ \ The routine also waits for SPACE or RETURN to be pressed before returning. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The number of the token to print as the header (see \ PrintHeader for more details): \ \ * 1 = "POINTS" \ \ * 2 = "GRID POSITIONS" \ \ * 3 = "ACCUMULATED POINTS" \ \ * 6 = "BEST LAP TIMES" \ \ A Defines what to show in the third column in the table: \ \ * 0 = lap times \ \ * 4 = points awarded in the last race \ \ * &88 = accumulated points \ \ positionNumber A list of position numbers (for race tables only), which \ contains the numbers 0 to 19 in sequence, with tied \ positions represented by shared position numbers \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ G Bit 7 is clear if RETURN was pressed, set if SPACE was \ pressed \ \ ****************************************************************************** .PrintDriverTable PHA \ Store the value of A on the stack so we can retrieve \ it below AND #%00001111 \ Set colourScheme to the lower nibble of A, which STA colourScheme \ contains the colour scheme to use for the table, so we \ can pass it to SetRowColours below JSR PrintHeader \ Print the header specified in X LDY #0 \ We are about to print a table containing 20 rows, one \ for each driver, so set a row counter in Y .dtab1 STY rowCounter \ Store the row counter in rowCounter LDA #%00000000 \ Set G = 0 so the call to Print2DigitBCD below will STA G \ print the second digit and will not print leading \ zeroes when printing the driver number JSR SetRowColours \ Set the colours in token 31 according to the colour \ scheme we stored in colourScheme above, so they can be \ used to set the colours of each table cell LDX #32 \ Print token 32, which prints two spaces and backspaces JSR PrintToken \ followed by token 31, so this sets up the colours for \ the first column LDY rowCounter \ Set Y to the table row number LDA positionNumber,Y \ Set A to the positionNumber for this row, to show in \ the first column BIT raceStarted \ If bit 7 of raceStarted is set, that means the results BMI dtab2 \ are from a finished race, so jump to dtab2 to skip the \ following instruction, so we print the numbers from \ positionNumber in the first column TYA \ Set A to the row number, so we print the row number in \ the first column (i.e. 1 to 20, as ) .dtab2 JSR ConvertNumberToBCD \ Convert the number in A into binary coded decimal \ (BCD), adding 1 in the process JSR Print2DigitBCD \ Print the binary coded decimal (BCD) number in A, and \ because we set G to 0 above, it will print the second \ digit and will not print leading zeroes LDX #31 \ Print token 31, which prints two spaces and sets the JSR PrintToken \ colours as configured above, so this inserts a black \ gap between the first and second table columns LDY rowCounter \ Set Y to the table row number JSR PrintPositionName \ Print the name of the driver in position Y, so row Y \ of the table contains the details of the driver in \ position Y, and set driverPrinted to the number of the \ driver that was printed LDX #31 \ Print token 31, which prints two spaces and sets the JSR PrintToken \ colours as configured above, so this inserts a black \ gap between the second and third table columns LDX driverPrinted \ Set X to the number of the driver we just printed, so \ the call to PrintTimer prints the lap time for driver \ X PLA \ If the value of A that we stored on the stack at the PHA \ start of the routine is non-zero, jump to dtab3 to BNE dtab3 \ skip the following \ If we get here then the value of A passed to the \ routine was 0, so we now print the third column \ containing the driver's best lap time LDA #%00100110 \ Print the 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 JMP dtab6 \ Jump down to dtab6 to move on to the next table row .dtab3 \ If we get here then the value of A passed to the \ routine was non-zero (i.e. 4 or &88) BMI dtab4 \ If bit 7 of A is set (i.e. A = &88), jump to dtab4 \ If we get here then the value of A passed to the \ routine was 4, so we now print the third column \ containing the points awarded to the driver in the \ last race \ \ Only the top six drivers from each race get points, \ so we print a blank column for the other drivers LDA rowCounter \ Set A = rowCounter + 20 CLC ADC #20 CMP #26 \ Set X = A, and if A < 26 (so rowCounter is 0 to 5), TAX \ jump to dtab5 to print the race points BCC dtab5 LDA #7 \ A >= 26 (so rowCounter is 6 to 19), so print seven JSR PrintSpaces \ spaces in the last column BEQ dtab6 \ Jump to dtab6 to move on to the next table row (this \ BEQ is effectively a JMP, as PrintSpaces sets the Z \ flag) .dtab4 \ If we get here then the value of A passed to the \ routine had bit 7 set, so it must have been &88, so \ we now print the third column containing the driver's \ accumulated points LDA #%00101000 \ Set G, so the next three calls to Print2DigitBCD do STA G \ the following: \ \ * No leading zeroes, print second digit \ * Leading zeroes, print second digit \ * Leading zeroes, print second digit \ \ The second and third two calls to Print2DigitBCD are \ in the Print4DigitBCD routine below LDA totalPointsTop,X \ If the top byte of the driver's total points is zero, BEQ dtab5 \ jump to dtab5 JSR Print2DigitBCD \ Otherwise print the top byte of the driver's total \ points, which is a binary coded decimal (BCD) number LDA totalPointsHi,X \ Fetch the high byte of the driver's total points, to \ pass to Print4DigitBCD JSR Print4DigitBCD \ Print both the high and low bytes of the driver's \ total points in full, followed by a space JMP dtab6 \ Jump to dtab6 to move on to the next table row .dtab5 JSR Print234DigitBCD \ Print the high and low bytes of the driver's total \ points, replacing leading zeroes with spaces, and \ followed by a space .dtab6 \ If we get here then we have finished printing the \ current table row, so now we move on to the next row LDY rowCounter \ Set Y to the table row number INY \ Increment the table row number CPY #20 \ Loop back to print the next table row, until we have BNE dtab1 \ printed all 20 LDA #3 \ Print three spaces to pad out the final row JSR PrintSpaces LDA #156 \ Print ASCII 156 to switch to a black background JSR OSWRCH LDA raceStarted \ If bit 7 of raceStarted is clear, that means the BPL dtab7 \ results are from qualifying or practice, so jump to \ dtab7 to skip the following so we do not print the \ race class above the table \ We now print the race class and number of laps in the \ gap between the page header and the top of the table LDX #49 \ Print token 49, which moves the cursor to column 9, JSR PrintToken \ row 2 JSR PrintRaceClass \ Print the race class LDA lapsMenuOption \ Set the configurable token in token 50 to 218 plus the CLC \ figure in lapsMenuOption, to give 218, 219 or 220, ADC #218 \ which correspond to the tokens for " 5", "10" and "20" STA token50+3 LDX #50 \ Print token 50, which is "n laps", where n is the JSR PrintToken \ number of laps we just configured .dtab7 PLA \ Retrieve the value of A that we stored on the stack at \ the start of the routine JSR WaitForSpaceReturn \ Print a prompt and wait for SPACE or RETURN to be \ pressed, depending on bit 7 of A RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: PrintNearestDriver \ Type: Subroutine \ Category: Text \ Summary: Print a driver's name in the "In front" or "Behind" slot in the \ header \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ Y The position of the driver whose name we print \ \ A The pixel row on which to print the driver name: \ \ * 24 = the first line of text at the top of the screen \ (i.e. the "In front:" section of token 43) \ \ * 33 = the second line of text at the top of the \ screen (i.e. the "Behind:" section of token 44) \ \ ****************************************************************************** .PrintNearestDriver STA yCursor \ Move the cursor to the pixel row in A LDA #27 \ Move the cursor to character column 27 STA xCursor \ Fall through into PrintPositionName to print the \ driver name at column 27 on the specified row \ ****************************************************************************** \ \ Name: PrintPositionName \ Type: Subroutine \ Category: Text \ Summary: Print the name of the driver in a specific position in the driver \ position list \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ Y The position of the driver whose name we print \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ driverPrinted The number of the driver that we printed \ \ ****************************************************************************** .PrintPositionName LDX driversInOrder,Y \ Set X to the number of the driver in position Y STX driverPrinted \ Store the driver number in driverPrinted, so we can \ return it JSR GetDriverAddress \ Set (Y A) to the address of driver X's name JSR PrintDriverName \ Print the name of the driver at address (Y A) RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: PrintDriverPrompt \ Type: Subroutine \ Category: Text \ Summary: Print the "DRIVER ->" prompt and a driver's name, to show whose \ turn it is next when playing a multi-player game \ \ ****************************************************************************** .PrintDriverPrompt LDX #29 \ Print token 29, which clears the screen, displays the JSR PrintToken \ F3 header, and shows a " DRIVER -> " prompt LDX currentPlayer \ Set X to the driver number of the current player JSR GetDriverAddress \ Set (Y A) to the address of driver X's name JSR PrintDriverName \ Print the name of the driver at address (Y A) JSR WaitForSpace \ Print a prompt and wait for SPACE to be pressed RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: AddRacePoints \ Type: Subroutine \ Category: Drivers \ Summary: Add the race points to the driver's total points \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The race position whose points should be added \ \ Y The driver who receives those points, i.e. who has then \ added to their total accumulated points \ \ ****************************************************************************** .AddRacePoints SED \ Set the D flag to switch arithmetic to Binary Coded \ Decimal (BCD) LDA totalPointsLo,Y \ Add (0 racePointsHi racePointsLo) for position X to CLC \ (totalPointsTop totalPointsHi totalPointsLo) for ADC racePointsLo,X \ driver Y, starting with the low bytes STA totalPointsLo,Y LDA totalPointsHi,Y \ And then the high bytes ADC racePointsHi,X STA totalPointsHi,Y LDA totalPointsTop,Y \ And then the top bytes ADC #0 STA totalPointsTop,Y CLD \ Clear the D flag to switch arithmetic to normal RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: ResetTrackLines \ Type: Subroutine \ Category: Screen buffer \ Summary: Reset the track lines below the horizon in the track view \ \ ------------------------------------------------------------------------------ \ \ This routine does the following: \ \ * Set horizonLine+1 bytes at rightGrassStart to &80 \ \ * Set horizonLine+1 bytes at leftVergeStart to &80 \ \ * Set horizonLine+1 bytes at rightVergeStart to &80 \ \ * Set horizonLine+1 bytes at leftTrackStart to &80 \ \ * Set 80 bytes at backgroundColour to 0 \ \ Setting each of the four verge edges to &80 effectively pushes the edges off \ the right edge of the screen, which resets the verge edges on that track line. \ By resetting the first horizonLine+1 bytes in each verge edge block, we are \ resetting all the edges for track lines below the horizon, i.e. from track \ line 0 at the bottom of the screen to track line horizonLine+1 on the horizon. \ \ Setting the 80 bytes at backgroundColour to zero resets the background colour \ for all track lines. \ \ ****************************************************************************** .ResetTrackLines LDX horizonLine \ We start by setting horizonLine+1 bytes at \ leftVergeStart, rightGrassStart rightVergeStart and \ leftTrackStart to &80, so set a byte counter in X LDA #&80 \ Set A = &80 to use as our reset value .resl1 STA leftVergeStart,X \ Set the X-th byte of leftVergeStart to &80 STA leftTrackStart,X \ Set the X-th byte of leftTrackStart to &80 STA rightVergeStart,X \ Set the X-th byte of rightVergeStart to &80 STA rightGrassStart,X \ Set the X-th byte of rightGrassStart to &80 DEX \ Decrement the byte counter BPL resl1 \ Loop back until we have zeroed all horizonLine+1 bytes LDX #79 \ We now zero the 80 bytes at backgroundColour, so set a \ byte counter in X LDA #0 \ Set A = 0 to use as our zero value .resl2 STA backgroundColour,X \ Zero the X-th byte of backgroundColour DEX \ Decrement the byte counter BPL resl2 \ Loop back until we have zeroed all 80 bytes RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: GetDriverName \ Type: Subroutine \ Category: Keyboard \ Summary: Fetch a player's name from the keyboard \ \ ****************************************************************************** .GetDriverName LDX currentPlayer \ Set X to the driver number of the current player JSR GetDriverAddress \ Set (Y A) to the address of driver X's name LDX #12 \ Fetch a string of length 12 from the keyboard and JSR GetTextInput \ store it in (Y A), padding the string out with spaces \ if required RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: DrawCars \ Type: Subroutine \ Category: Drawing objects \ Summary: Draw all the car objects, with four objects for the closest car in \ front of us \ \ ****************************************************************************** .DrawCars LDX currentPosition \ Set X to the current player's position, so we work our \ way backwards through the pack, starting with the car \ behind the current player, and wrapping round to the \ cars in front, working our way towards the player's \ car in the order in which they should be drawn (with \ distant cars first) BPL cars2 \ If X is positive, jump to cars2 to skip the following \ instruction .cars1 JSR DrawCarInPosition \ Draw the car in position X .cars2 JSR GetPositionBehind \ Set X to the number of the position behind position X, \ so we work our way back through the pack CPX positionAhead \ Loop back to cars1 until we have reached the position BNE cars1 \ ahead of the current player \ We now draw the car that's just in front of us, which \ is made up of four objects that can be skewed to make \ it look like the car is steering LDX #22 \ The four objects are the front tyres, body, rear tyres \ and rear wing, so set up a counter in X to work \ through the first three in the order 22, 21 and 20, to \ pass to DrawCarOrSign in turn so they get drawn in \ that order: front tyres, body/helmet and rear tyres .cars3 STX xStoreDraw \ Store X in xStoreDraw so it gets preserved through \ the call to DrawCarOrSign JSR DrawCarOrSign \ Draw the specified part of the four-object car just \ in front of us DEX \ Decrement the object counter CPX #20 \ Loop back until we have drawn all three objects BCS cars3 LDX positionAhead \ Set X to the position ahead of the current player JSR DrawCarInPosition \ Draw the car in position X, which draws the rear wing \ as the last (and closest) of the four objects RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: dashData42 \ Type: Variable \ Category: Screen buffer \ Summary: Contains part of the dashboard image that gets moved into screen \ memory \ \ ****************************************************************************** ORG &6C00 .dashData42 SKIP 2853 \ ****************************************************************************** \ \ Name: UpdateMirrors \ Type: Subroutine \ Category: Dashboard \ Summary: Update the wing mirrors to show any cars behind us \ Deep dive: Wing mirrors \ \ ****************************************************************************** ORG &7B00 .UpdateMirrors LDY positionBehind \ Set Y to the position of the driver behind us LDX driversInOrder,Y \ Set X to the number of the driver behind us LDA objectStatus,X \ If the object status byte for the car behind us has BMI upmi1 \ bit 7 set, then it is hidden, so jump to upmi1 to \ clear the mirror (as V will be set to a negative \ value, and this will never match any values from \ mirrorSegment, as all the mirrorSegment entries are \ positive) \ We now calculate the size of the car to draw in the \ mirror (in other words, the height of the block we \ draw) \ \ We do this by taking the scale factor for the driver \ behind (which is the same as the size of the car) and \ dividing by 8 to give us half the number of pixel \ lines to draw, in T (so if T = 1, we draw a block \ that's two pixels high, and if T = 4, we draw a block \ that's eight pixels high) \ \ We then calculate the upper and lower offsets within \ the mirror segment, by taking the offset of the middle \ row in the segment, and adding and subtracting T to \ give us T rows either side of &B6 in TT and N \ \ We then pass N and TT (the latter via A) into the \ DrawCarInMirror routine LDA objectSize,X \ Set A = the size of the object for the driver behind LSR A \ Set T = A / 8 LSR A \ = objectSize / 8 LSR A STA T CLC \ Set TT = &B6 + T ADC #&B6 STA TT LDA #&B6 \ Set N = &B6 - T SEC SBC T STA N \ Next we calculate the mirror segment that the car \ should appear in, based on the difference in yaw angle \ between the car (whose angle is in objYawAngleHi), and \ the player (whose angle is in playerYawAngleHi), \ storing the result in A \ \ This gives us the amount of distance that the car \ behind the player is to either side of the player, in \ terms of yaw angle \ \ This can then be matched with the values in the \ mirrorSegment table to see which segment we should \ show the car in, with larger yaw angles mapping to \ segments towards the outside of the mirrors LDA objYawAngleHi,X \ Set A = objYawAngleHi for the driver behind SEC \ Set A = A - playerYawAngleHi - 4 SBC playerYawAngleHi \ = objYawAngleHi - playerYawAngleHi - 4 SEC SBC #4 LSR A \ Set A = A >> 3 LSR A \ = A div 8 LSR A .upmi1 STA V \ Set V = A \ So by this point: \ \ * V is negative if there is no car in the mirror (so \ we must have jumped here from the BMI at the start \ of the routine) \ \ * Otherwise V is a yaw angle that potentially maps to \ a mirror segment number LDY #5 \ We now loop through the six mirror segments, either \ clearing or drawing each of them, so we set up a loop \ counter in Y to count from 5 to 0 .upmi2 LDA V \ If V matches this segment's mirrorSegment value, then CMP mirrorSegment,Y \ we can see a car in this segment, so jump to upmi3 to BEQ upmi3 \ set A = TT (which we calculated above to denote the \ size of the car) and send this to mirrorContents and \ DrawCarInMirror \ If we get here then we can't see a car in this mirror \ segment, so we need to clear the mirror to white LDA mirrorContents,Y \ If this segment's mirrorContents value is 0, then BEQ upmi5 \ there is no car being shown in this segment, so jump \ to upmi5 to move on to the next segment, as the \ mirror segment is already clear LDA #0 \ Otherwise we need to clear this segment, so set A = 0 BEQ upmi4 \ and jump to upmi4 to send this to mirrorContents and \ DrawCarInMirror (this BEQ is effectively a JMP as A is \ always zero) .upmi3 LDA TT \ Set A = TT to store in mirrorContents and pass to \ DrawCarInMirror .upmi4 STA mirrorContents,Y \ Store A in the Y-th entry in mirrorContents, which \ will be zero if there is no car in this mirror \ segment, non-zero if there is JSR DrawCarInMirror \ Draw the car in the specified mirror segment, between \ the upper and lower offsets in A and N .upmi5 DEY \ Decrement the loop counter BPL upmi2 \ Loop back until we have looped through 5 to 0 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: ShowStartingLights \ Type: Subroutine \ Category: Dashboard \ Summary: Show the lights at the start of the race \ Deep dive: Starting lights \ \ ------------------------------------------------------------------------------ \ \ The lights at the start of the race follow this pattern: \ \ * The lights are initially off, so they show as three lights with a white \ border and black interior. \ \ * Once we have started our engine, the timer starts and after a pause the \ lights turn blue. During this time we can't move, but we can rev the \ engine. \ \ * After another pause, the lights turn green and we can start the race. \ \ * After another pause, the lights disappear. \ \ This is implemented by the counter in raceStarting, which works like this. \ \ When a race is about to start, raceStarting is set to 128, and stays on this \ value until the engine is started, at which point it starts to count down, \ with one tick per iteration of the main loop, working through the following \ sequence: \ \ * 128 = show black lights, engine not yet started \ \ * Start counting down from 240 once engine starts \ \ * 240-192 = show black lights \ \ * 191-161 = show blue lights \ \ * 160 = keep showing blue lights and stop counting down until main loop \ counter is a multiple of 64 \ \ * Start counting down from 40 once loop counter is a multiple of 64 \ \ * 40-1 = show green lights \ \ * 0 = show no lights (race has started) \ \ ****************************************************************************** .ShowStartingLights LDA raceStarted \ If bit 7 of raceStarted is clear then this is either BPL star9 \ a practice or qualifying lap, so jump to star9 to \ return from the subroutine, as there is no need for \ starting lights LDX raceStarting \ If raceStarting = 0, then the race is not in the BEQ star9 \ process of starting (i.e. it's already started), so \ jump to star9 to return from the subroutine, as there \ is no need for starting lights BPL star2 \ If raceStarting <= 127, jump to star2 to decrement \ the raceStarting counter (this instruction isn't \ necessary as the following comparison covers the same \ test, so this branch could be removed) CPX #128 \ If raceStarting <> 128, jump to star2 to decrement BNE star2 \ the raceStarting counter \ If we get here then raceStarting = 128, which means \ the lights are showing as black, and we are waiting \ for the engine to start to continue the countdown BIT engineStatus \ If bit 7 of engineStatus is clear, then the engine is BPL star1 \ not yet on, so jump to star1 to keep the value of X \ (and therefore the value of raceStarting) at 128, as \ we only move on to the second stage once we have \ started our engine LDX #240 \ If we get here then the engine has started, so set \ X = 240 to store as the updated value of raceStarting, \ which can now start ticking down .star1 \ If we get here then the lights are off LDY #0 \ Set Y to the EOR pattern that doesn't flip any pixels, \ \ See star8 below for an explanation of the EOR pattern LDA #%10000000 \ Set A to a pixel byte containing four pixels with \ colours 2, 0, 0, 0 (white, black, black, black) BNE star6 \ Jump to star6 to store the updated value of X in \ raceStarting and draw the lights (this BNE is \ effectively a JMP as A is never zero) .star2 \ If we get here, then X is non-zero and X <> 128, so \ the engine has started and the light sequence has \ started CPX #160 \ If X = 160, jump to star4 to consider switching from BEQ star4 \ the blue lights to the green lights DEX \ Decrement X BPL star5 \ If X is positive, jump to star5 to display the green \ lights CPX #192 \ If X >= 192, jump to star1 BCS star1 .star3 \ If we get here then the lights are blue (the second \ stage) LDA #%10100101 \ Set A to a pixel byte containing four pixels with \ colours 2, 1, 2, 1 (white, blue, white, blue) LDY #%01110111 \ Set Y to the EOR pattern that flips the above to \ %11010010, i.e. colours 2, 2, 1, 2 (white, white, \ blue, white) \ \ See star8 below for an explanation of the EOR pattern BNE star6 \ Jump to star6 to store the updated value of X in \ raceStarting and draw the lights (this BNE is \ effectively a JMP as A is never zero) .star4 LDA mainLoopCounterLo \ If mainLoopCounterLo mod 64 <> 0, which will be true AND #63 \ for 63 out of 64 iterations round the main loop, jump BNE star3 \ to star3 to display the blue lights \ Otherwise it is time to switch on the green lights LDX #40 \ Set X = 40 .star5 \ If we get here then the lights are green (the third \ and final stage) LDA #%11110010 \ Set A to a pixel byte containing four pixels with \ colours 2, 2, 3, 2 (white, white, green, white) LDY #%00000101 \ Set Y to the EOR pattern that flips the above to \ %11110111, i.e. colours 2, 3, 3, 3 (white, green, \ green, green) \ \ See star8 below for an explanation of the EOR pattern .star6 STX raceStarting \ Store the updated value of X in raceStarting \ We now draw the lights, starting with the white border \ lines, and then the lights themselves using the pixel \ byte in A and the EOR pattern in Y \ \ We only need to draw the left set of lights, as the \ screen buffer will replicate the other two sets to the \ right \ \ The whole light takes up ten pixel rows: two rows for \ the top edge, six rows for the light bulb, and another \ two rows for the bottom edge PHA \ Store the pixel byte in A on the stack, so we can \ retrieve it below when drawing the lights STY T \ Store the EOR pattern in T, so we can retrieve it when \ drawing the lights LDA #%11110000 \ Set A to a pixel byte containing four pixels with \ colours 2, 2, 2, 2 (white, white, white, white), for \ drawing the edges, which we actually draw as a full \ block of white, and then fill in the centre later LDX #9 \ The light contains ten pixel rows, so set a row \ counter in X .star7 STA dashData37+36,X \ Store the X-th row of the light in the screen buffer, \ in dash data block 37 (the screen drawing routine will \ replicate the light in blocks 38 and 39) DEX \ Decrement the pixel row counter BPL star7 \ Loop back until we have drawn all ten rows of the \ light PLA \ Set A to the pixel byte for the lights that we stored \ on the stack above LDX #5 \ The central bulb of the light is six pixel rows, so \ set a row counter in X .star8 STA dashData37+38,X \ Store the X-th row of the light in the screen buffer, \ again in dash data block 37, starting two pixel rows \ after the edge that we drew above EOR T \ Flip the pixels by EOR'ing with T, so the pattern of \ the bulb pixels flips with each row required \ \ This means the green light looks like this: \ \ white, white, green, white ..x. \ white, green, green, green .xxx \ white, white, green, white ..x. \ white, green, green, green .xxx \ white, white, green, white ..x. \ white, green, green, green .xxx \ \ and the blue light looks like this: \ \ white, blue, white, blue .x.x \ white, white, blue, white ..x. \ white, blue, white, blue .x.x \ white, white, blue, white ..x. \ white, blue, white, blue .x.x \ white, white, blue, white ..x. \ \ The EOR pattern for the black lights is 0, so there is \ no flipping and every row is the same: \ \ white, black, black, black .xxx \ white, black, black, black .xxx \ white, black, black, black .xxx \ white, black, black, black .xxx \ white, black, black, black .xxx \ white, black, black, black .xxx DEX \ Decrement the pixel row counter BPL star8 \ Loop back until we have drawn all six rows of the \ bulb .star9 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: PrintTimer \ Type: Subroutine \ Category: Text \ Summary: Print the specified timer \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The lap time to print: \ \ * 0 to 19: Lap time for the specified driver \ \ * 20 = the clock timer \ (clockMinutes clockSeconds clockTenths) \ \ * 21 = the lap timer \ (lapMinutes lapSeconds lapTenths) \ \ A Flags to control how the time is printed: \ \ * Bit 7: clear = do not print leading zeroes in mins \ set = print leading zeroes in mins \ \ * Bit 6: clear = print second digit in mins \ set = do not print second digit in mins \ \ * Bit 5: clear = do not print leading zeroes in secs \ set = print leading zeroes in secs \ \ * Bit 4: clear = print second digit in secs \ set = do not print second digit in secs \ \ * Bit 3: clear = print tenths of a second \ set = do not print tenths of a second \ \ * Bit 2: clear = do not print leading zeroes in tenths \ set = print leading zeroes in tenths \ \ * Bit 1: clear = print second digit in tenths \ set = do not print second digit in tenths \ \ ****************************************************************************** .PrintTimer STA G \ Store A in G so we can check the value of bit 7 below LDA bestLapMinutes,X \ Print the number of minutes in driver X's best lap JSR Print2DigitBCD \ time LDA #&3A \ Print ":" JSR PrintCharacter LDA bestLapSeconds,X \ Print the number of seconds in driver X's best lap JSR Print2DigitBCD \ time ASL G \ If bit 7 of G is set, we do not want to print tenths BCS plap1 \ of a second, so jump to plap1 to return from the \ subroutine LDA #&2E \ Print "." JSR PrintCharacter LDA bestLapTenths,X \ Print the number of tenths of a second in driver X's JSR Print2DigitBCD \ best lap time .plap1 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: DrawTrackView (Part 4 of 4) \ Type: Subroutine \ Category: Screen buffer \ Summary: Revert all the code modifications made by the DrawTrackView \ routine \ Deep dive: Drawing around the dashboard \ Drawing the track view \ \ ****************************************************************************** .view19 LDA view3+1 \ Modify the instruction at view20 to use the low byte STA view20+1 \ of the address from view3 \ \ In part 2 we modified the instruction at view3 to \ revert the specified instruction back to STA (P),Y, \ ready for a loop-back that never happened, so view20 \ will now revert this change instead LDA view8+1 \ Modify the instruction at view21 to use the low byte STA view21+1 \ of the address from view8 \ \ In part 3 we modified the instruction at view8 to \ revert the specified instruction back to STA (P),Y, \ ready for a loop-back that never happened, so view21 \ will now revert this change instead LDA view14+1 \ Modify the instruction at view22 to use the low byte STA view22+1 \ of the address from view14 \ \ In part 3 we modified the instruction at view14 to \ revert the specified instruction back to STA (P),Y, \ ready for a loop-back that never happened, so view22 \ will now revert this change instead LDA #&91 \ Set A to the opcode for the STA (P),Y instruction .view20 STA DrawTrackBytes+15 \ Revert the instruction that view3 would have reverted .view21 STA DrawTrackBytes+15 \ Revert the instruction that view8 would have reverted .view22 STA byte2+15 \ Revert the instruction that view14 would have reverted LDA #&E0 \ Set A to the opcode for the CPX #44 instruction STA byte3 \ Revert the instruction at byte3 to CPX #44 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: DrawTrackView (Part 1 of 4) \ Type: Subroutine \ Category: Screen buffer \ Summary: Draw the top part of the track view using the data in the dash \ data blocks \ Deep dive: Drawing around the dashboard \ Drawing the track view \ The track verges \ \ ****************************************************************************** .DrawTrackView LDA #0 \ Set (Q P) = &6700 and (S R) = &6800, ready to pass to STA P \ the DrawTrackLine routine (so the track view gets STA R \ drawn at the correct place on screen, from &6701 LDX #&67 \ onwards, as the first line is drawn at (Q P) + 1) STX Q INX STX S LDX #79 \ Set X = 79, to point to the first byte to draw from \ each dash data block (i.e. the byte at the end of the \ data block, at offset 79) JSR DrawTrackLine \ Draw one-pixel high lines that correspond to dash data \ offsets 79 to 44, returning from the subroutine after \ drawing the line specified by offset X = 44 (so this \ draws all the lines from the top of the track view, \ down to the line just above the top of the dashboard) JMP view1 \ Jump to part 2 to draw the rest of the track view from \ offsets 43 to 3, modifying the code so it draws the \ rest of the lines around the shape of the dashboard \ ****************************************************************************** \ \ Name: DrawTrackLine (Part 2 of 2) \ Type: Subroutine \ Category: Screen buffer \ Summary: Draw a pixel line across the screen in the track view \ Deep dive: Drawing around the dashboard \ Drawing the track view \ \ ****************************************************************************** .prow2 \ At this point, X contains the offset within the dash \ data of the pixel line to be drawn across the screen LDA backgroundColour,X \ Fetch the colour of the first byte on the line from AND #%00000011 \ the X-th entry in backgroundColour (bits 0 to 2) TAY LDA colourPalette,Y \ Set A to logical colour Y from the colour palette, \ to use as the first byte on the line \ Fall through into DrawTrackBytes to draw the pixel \ bytes that make up the line we want to draw \ \ If we are drawing the lines above the top of the \ dashboard, then the following then loops back to the \ start of the DrawTrackLine routine to keep drawing \ lines until we do reach the top of the dashboard, at \ which point we return from the DrawTrackLine routine \ to part 2 of DrawTrackView, which modifies the code to \ draw subsequent lines around the shape of the \ dashboard \ ****************************************************************************** \ \ Name: DRAW_BYTE \ Type: Macro \ Category: Screen buffer \ Summary: Draw a pixel byte as part of a horizontal line when drawing the \ track view \ \ ------------------------------------------------------------------------------ \ \ This routine draws a row of four pixels (i.e. one byte) as part of the track \ view. \ \ The track view is drawn one line at a time, with each line being one pixel \ high. Each of these lines is drawn as a sequence of bytes, with each byte \ containing four pixels. \ \ Each macro instance draws one pixel byte into screen memory, so that's one \ four-pixel block within the horizontal line. So the full sequence of macros, \ from DRAW_BYTE 0 through DRAW_BYTE 39, draws a one-pixel high line across \ the full width of the screen. In other words, each DRAW_BYTE macro draws a \ character block's worth of line, as the screen is 40 character blocks wide. \ \ Each macro instance draws one pixel byte, from offset X within dash data block \ I%, into screen memory. The offset X is decremented for each run through the \ sequence of macros, as data is stored at the end of each dash data block. So \ as each pixel line is drawn, moving down the screen, X decrements down from 79 \ (the start of each dash data block) as we work our way through the data. \ \ The destination address in screen memory for the data is governed by (Q P), \ which points to the address of the pixel byte to update in the first byte \ (i.e. the address of the first pixel in the line across the screen). \ \ If the dash data byte is zero, then the current value of A is stored in screen \ memory (i.e. the same value that was stored in the previous byte). \ \ If the dash data byte is non-zero, then this is stored in A and screen memory, \ unless it is &55, in which a zero is stored in A and screen memory. \ \ As it is copied, the source dash data byte is zeroed, so the macro effectively \ moves a byte into screen memory, clearing it in the process. \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ I% The pixel byte number (0 to 39) \ \ X The offset within the dash data of the data to be drawn \ on the screen (from &7F down, as the dash data lives at \ the end of each dash data block) \ \ A The value stored in screen memory in the previous pixel \ byte (or the starting value if this is pixel byte 0) \ \ (Q P) The screen address of the leftmost character block to \ update (i.e. the screen address of the pixel byte to \ draw in the first character block on the line, so this \ is the address of the start of the horizontal line that \ the sequential macros draw) \ \ (S R) Contains (Q P) + &100, to be used instead of (Q P) for \ pixel bytes 32 to 39 \ \ ****************************************************************************** MACRO DRAW_BYTE I% LDY dashData+&80*I%,X \ Set Y to the X-th byte of dash data block I% BEQ P%+10 \ If Y = 0, skip the next three instructions (i.e. jump \ to the LDY #LO(8*I%) instruction to leave the value of \ A alone) \ Otherwise Y is non-zero, and we do the following in \ the next three instructions: \ \ * Zero the location in code block I% from which we \ just read a byte \ \ * Set A to the byte we just read from code block I%, \ unless the value is &55, in which case set A = 0 LDA #0 \ Zero the X-th byte of dash data block I% STA dashData+&80*I%,X LDA zeroIfYIs55,Y \ Set A to the Y-th byte from zeroIfYIs55 \ \ This sets A = Y, unless Y = &55, in which case it sets \ Y = 0, so that's: \ \ If Y = &55, set A = 0, otherwise set A = Y \ \ The zeroIfYIs55 table exists just to enable us to do \ this logic in one instruction \ If Y = 0 above, this is where we jump to LDY #LO(8*I%) \ Store A in character block I% in screen memory, as an IF I% < 32 \ offset from (Q P) STA (P),Y \ ELSE \ (S R) is used instead of (Q P) for pixel bytes 32 to STA (R),Y \ 39, which saves us from having to increment Q to cross ENDIF \ the page boundary, as (S R) = (Q P) + &100 \ Fall through into the next DRAW_BYTE macro to draw \ the next pixel byte along to the right, continuing the \ horizontal line of pixels ENDMACRO \ ****************************************************************************** \ \ Name: DrawTrackBytes (Part 1 of 3) \ Type: Subroutine \ Category: Screen buffer \ Summary: Draw the pixel bytes that make up the track view (0 to 15) \ Deep dive: Drawing around the dashboard \ Drawing the track view \ \ ------------------------------------------------------------------------------ \ \ This routine draws pixel bytes 0 to 39, which draws a one-pixel high line in \ the track view. \ \ Note that this routine starts on a page boundary (DrawTrackBytes = &7C00). \ This is important as it means the code can be modified at a specific address \ using only the low byte of that address, as we know that high byte is the same \ throughout the routine. This is why the staDrawByte and ldaDrawByte lookup \ tables only need to store the low bytes of the addresses for instructions that \ we need to modify. \ \ ****************************************************************************** .DrawTrackBytes DRAW_BYTE 0 \ Draw pixel bytes 0 to 15 DRAW_BYTE 1 DRAW_BYTE 2 DRAW_BYTE 3 DRAW_BYTE 4 DRAW_BYTE 5 DRAW_BYTE 6 DRAW_BYTE 7 DRAW_BYTE 8 DRAW_BYTE 9 DRAW_BYTE 10 DRAW_BYTE 11 DRAW_BYTE 12 DRAW_BYTE 13 DRAW_BYTE 14 DRAW_BYTE 15 JMP byte1 \ Jump to part 2 to continue with pixel byte 16 \ ****************************************************************************** \ \ Name: DrawTrackView (Part 2 of 4) \ Type: Subroutine \ Category: Screen buffer \ Summary: Draw the part of the track view that fits around the dashboard \ Deep dive: Drawing around the dashboard \ Drawing the track view \ \ ------------------------------------------------------------------------------ \ \ This routine modifies the DrawTrackBytes routine so that it draws all the \ remaining lines in the track view so they fit around the shape of the \ dashboard. \ \ ****************************************************************************** .view1 \ We get here with X = 44, as in part 1 we drew the \ lines specified by dash data offsets 79 to 44 \ \ In the following loop, we draw the lines specified by \ dash data offsets 43 to 28 LDA #&60 \ Set A to the opcode for the RTS instruction STA byte3 \ Modify the following instruction in DrawTrackBytes \ (part 3): \ \ CPX #44 -> RTS \ \ so that calls to DrawTrackLine and DrawTrackBytes from \ now on will draw individual lines rather than looping \ back to DrawTrackLine as they did for the lines above \ the dashboard .view2 DEX \ Decrement the dash data block pointer to point to \ the data for the next line LDY staDrawByte,X \ Set Y to the X-th entry in staDrawByte, which contains \ the low byte of the address of the STA (P),Y \ instruction in the DRAW_BYTE macro given in the \ table CPY view3+1 \ If the instruction at view3 has already been modified BEQ view5 \ to this address, jump to view5 to skip the following \ modifications, as they have already been done on the \ previous iteration of the loop LDA #&91 \ Set A to the opcode for the STA (P),Y instruction .view3 STA DrawTrackBytes+15 \ Modify the specified instruction back to STA (P),Y \ (the address of the modified instruction is set by the \ following, so the first time we run this line it has \ no effect) STY view4+1 \ Modify the instruction at view4 to change the low byte \ of the address to the X-th entry in staDrawByte, so \ the instruction at view4 changes the STA (P),Y \ instruction to an RTS in the DRAW_BYTE macro given \ in staDrawByte STY view3+1 \ Modify the instruction at view3 to change the low byte \ of the address to the X-th entry in staDrawByte, so \ the instruction at view3 changes the RTS instruction \ back to STA (P),Y when we loop back around LDA #&60 \ Set A to the opcode for the RTS instruction .view4 STA DrawTrackBytes \ Modify the specified instruction to an RTS so the next \ call to DrawTrackLine will return at that point (the \ address of the modified instruction is set above) LDY ldaDrawByte,X \ Set Y to the X-th entry in ldaDrawByte, which contains \ the low byte of the LDA #0 instruction in the specific \ DRAW_BYTE macro, as given in the table STY view6+1 \ Modify the instruction at view6 to change the low byte \ of the address to the X-th entry in ldaDrawByte, so \ the instruction at view6 changes so it jumps to the \ LDA #0 instruction in the DRAW_BYTE macro specified in \ the table .view5 JSR DrawTrackLine \ Draw the left portion of this track line \ \ This routine was modified above to return from the \ subroutine at the STA instruction in the DRAW_BYTE \ macro specified in the staDrawByte table, so this \ returns the last pixel byte of this portion of the \ line in A, i.e. the rightmost byte of the left portion \ of the track line, where the line meets the left \ border of the central part of the dashboard AND leftDashMask,X \ We now merge the track byte in A with the left edge ORA leftDashPixels,X \ of the dashboard, by masking out the pixels in A that STA (P),Y \ are hidden by the dashboard (with AND leftDashMask), \ and replacing them with the pixels from the left edge \ of the dashboard (with ORA leftDashPixels) LDA dashRightEdge,X \ Fetch the track pixel byte that would be shown along \ the right edge of the dashboard, i.e. the leftmost \ byte of the right portion of the track line, where the \ line meets the right border of the central part of the \ dashboard AND rightDashMask,X \ We now merge the track byte in A with the right edge ORA rightDashPixels,X \ of the dashboard, by masking out the pixels in A that \ are hidden by the dashboard (with AND rightDashMask), \ and replacing them with the pixels from the left edge \ of the dashboard (with ORA rightDashPixels) TAY \ Copy the pixel byte into Y, because the following JSR \ jumps straight to the LDA #0 instruction within the \ DRAW_BYTE macro, and at that point the macro expects \ the pixel byte to be in Y rather than A .view6 JSR byte2 \ Draw the right portion of this track line \ \ This JSR was modified above to jump to the LDA #0 \ instruction in the DRAW_BYTE macro specified in the \ ldaDrawByte table CPX #28 \ Loop back to keep drawing lines, working our way down BNE view2 \ through the dash data from entry 44 down to entry 28 JMP view7 \ Jump to part 3 to draw the rest of the track view from \ offsets 27 to 3, modifying the code so it draws the \ rest of the lines around the shape of the dashboard \ and the shape of the tyres \ ****************************************************************************** \ \ Name: DrawTrackBytes (Part 2 of 3) \ Type: Subroutine \ Category: Screen buffer \ Summary: Draw the pixel bytes that make up the track view (16 to 39) \ Deep dive: Drawing around the dashboard \ Drawing the track view \ \ ------------------------------------------------------------------------------ \ \ Note that the latter half of this routine, from .byte2 onwards, starts on a \ page boundary (byte2 = &7E00). This is important as it means the code can be \ modified at a specific address using only the low byte of that address, as we \ know that high byte is the same throughout the routine. This is why the lookup \ tables at staDrawByte and ldaDrawByte only need to store the low bytes of \ the addresses for instructions that we need to modify. \ \ ------------------------------------------------------------------------------ \ \ Other entry points: \ \ byte2 Only draw pixel bytes 26 to 39 \ \ ****************************************************************************** .byte1 DRAW_BYTE 16 \ Draw pixel bytes 16 to 25 DRAW_BYTE 17 DRAW_BYTE 18 DRAW_BYTE 19 DRAW_BYTE 20 DRAW_BYTE 21 DRAW_BYTE 22 DRAW_BYTE 23 DRAW_BYTE 24 DRAW_BYTE 25 .byte2 DRAW_BYTE 26 \ Draw pixel bytes 26 to 39 DRAW_BYTE 27 DRAW_BYTE 28 DRAW_BYTE 29 DRAW_BYTE 30 DRAW_BYTE 31 DRAW_BYTE 32 DRAW_BYTE 33 DRAW_BYTE 34 DRAW_BYTE 35 DRAW_BYTE 36 DRAW_BYTE 37 DRAW_BYTE 38 DRAW_BYTE 39 .byte3 CPX #44 \ If X = 44, then we have just drawn the last pixel BEQ byte4 \ line above the top of the dashboard, so return from \ the subroutine so we can modify the routine to draw \ subsequent lines in two parts, to fit around the \ dashboard (as byte4 contains an RTS) DEX \ Decrement the dash data pointer in X to move on to the \ next pixel line \ Fall through into DrawTrackLine to draw the next line \ ****************************************************************************** \ \ Name: DrawTrackLine (Part 1 of 2) \ Type: Subroutine \ Category: Screen buffer \ Summary: Draw a pixel line across the screen in the track view, broken up \ into bytes \ Deep dive: Drawing around the dashboard \ Drawing the track view \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ X The offset within the dash data of the data to be drawn \ on the screen (from &7F down, as the dash data lives at \ the end of each dash data block) \ \ (Q P) The screen address of the leftmost pixel of the line \ above where we want to draw the horizontal pixel line \ \ (S R) Contains (Q P) + &100 \ \ ****************************************************************************** .DrawTrackLine \ We start by incrementing (Q P) and (S R) to point to \ the next pixel row down the screen LDY P \ Set Y = P + 1, which is the low byte of (Q P) + 1 INY TYA \ If Y mod 8 = 0 then incrementing (Q P) will take us AND #&07 \ into the next character block (i.e. from pixel row 7 BEQ prow1 \ to pixel row 8), so jump to prow1 to update the screen \ addresses accordingly STY P \ Otherwise set the low bytes of (Q P) and (S R) to Y, STY R \ so this does: \ \ (Q P) = (Q P) + 1 \ \ (S R) = (S R) + 1 \ \ so they point to the next pixel row down the screen JMP prow2 \ Jump to part 2 to draw this pixel row .prow1 TYA \ Set (Q P) = Y + &138 CLC \ ADC #&38 \ starting with the low bytes STA P STA R LDA Q \ And then the high bytes, so (Q P) points to the start ADC #&01 \ of the character block on the next character row STA Q \ (i.e. the next pixel row down) ADC #&01 \ Set (S R) = (Q P) + &100 STA S JMP prow2 \ Jump to part 2 to draw this pixel row \ ****************************************************************************** \ \ Name: DrawTrackBytes (Part 3 of 3) \ Type: Subroutine \ Category: Screen buffer \ Summary: Return from the subroutine \ Deep dive: Drawing around the dashboard \ Drawing the track view \ \ ****************************************************************************** .byte4 RTS \ Return from the subroutine \ ****************************************************************************** \ \ Name: DrawTrackView (Part 3 of 4) \ Type: Subroutine \ Category: Screen buffer \ Summary: Draw the part of the track view that fits around the dashboard and \ tyres \ Deep dive: Drawing around the dashboard \ Drawing the track view \ \ ------------------------------------------------------------------------------ \ \ This routine modifies the DrawTrackBytes routine so that it draws all the \ remaining lines in the track view so they fit around the shape of the \ dashboard and the tyres. \ \ ****************************************************************************** \ We get here with X = 28, as in part 1 we drew the \ lines specified by dash data offsets 79 to 44, and in \ part 2 we drew the lines specified by dash data \ offsets 43 to 28 \ \ In the following loop, we draw the lines specified by \ dash data offsets 27 to 3 .view7 DEX \ Decrement the dash data block pointer to point to \ the data for the next line LDY staDrawByte,X \ Set Y to the X-th entry in staDrawByte, which contains \ the low byte of the address of the STA (P),Y \ instruction in the DRAW_BYTE macro given in the \ table CPY view8+1 \ If the instruction at view8 has already been modified BEQ view10 \ to this address, jump to view10 to skip the following \ modifications, as they have already been done on the \ previous iteration of the loop LDA #&91 \ Set A to the opcode for the STA (P),Y instruction .view8 STA DrawTrackBytes+15 \ Modify the specified instruction back to STA (P),Y \ (the address of the modified instruction is set by the \ following, so the first time we run this line it has \ no effect) STY view9+1 \ Modify the instruction at view9 to change the low byte \ of the address to the X-th entry in staDrawByte, so \ the instruction at view9 changes the STA (P),Y \ instruction to an RTS in the DRAW_BYTE macro given \ in staDrawByte STY view8+1 \ Modify the instruction at view8 to change the low byte \ of the address to the X-th entry in staDrawByte, so \ the instruction at view8 changes the RTS instruction \ back to STA (P),Y when we loop back around LDA #&60 \ Set A to the opcode for the RTS instruction .view9 STA DrawTrackBytes \ Modify the specified instruction to an RTS so the next \ call to DrawTrackLine will return at that point (the \ address of the modified instruction is set above) .view10 \ The following code is normally run at the start of the \ DrawTrackLine routine, but we are going to call the \ DrawTrackBytes routine to draw our line instead, so \ we can skip the bytes that are hidden behind the left \ tyre \ \ So we repeat the code here, which increments the \ screen addresses in (Q P) and (S R) to point to the \ next pixel row down the screen LDY P \ Set Y = P + 1, which is the low byte of (Q P) + 1 INY TYA \ If Y mod 8 <> 0 then incrementing (Q P) will keep us AND #&07 \ within the current character block (i.e. the new pixel BNE view11 \ row will be 7 or less), so jump to view11 to store the \ incremented values \ Otherwise incrementing (Q P) will take us into the \ next character block (i.e. from pixel row 7 to pixel \ row 8), so we need to update the screen addresses to \ jump to the next character row TYA \ Set (Q P) = Y + &138 CLC \ ADC #&38 \ starting with the low bytes STA P STA R LDA Q \ And then the high bytes, so (Q P) points to start of ADC #&01 \ the character block on the next character row (i.e. STA Q \ the next pixel row down) ADC #&01 \ Set (S R) = (Q P) + &100 STA S BCC view12 \ Jump to view12 to skip the following .view11 \ If we get here then incrementing the screen addresses \ keeps us within the current character block, so we can \ store the incremented addresses in (Q P) and (S R) STY P \ Set the low bytes of (Q P) and (S R) to Y, so this STY R \ does: \ \ (Q P) = (Q P) + 1 \ \ (S R) = (S R) + 1 \ \ so they point to the next pixel row down the screen .view12 \ The staDrawByteTyre table contains the low byte offset \ of the address of the STA (P),Y instruction for the \ track line, which we convert into an RTS when drawing \ the track line up against the right tyre, so we stop \ in time (see the code that modifies view14 and view15 \ below) \ \ As the tyres are reflections of each other, we can \ also use this value to calculate the starting point \ for the line that starts at the left tyre, which is \ what we do now LDA #LO(byte3)+3 \ Set A = LO(byte3) + 3 - staDrawByteTyre SEC \ SBC staDrawByteTyre,X \ There are 14 instances of the DRAW_BYTE macro between \ byte2 and byte3, ranging from DRAW_BYTE 26 up to \ DRAW_BYTE 39 \ \ This calculation converts the low address byte from \ the staDrawByteTyre table so that instead of pointing \ to the LDA #0 instruction in the n-th DRAW_BYTE macro, \ it points to the LDA #0 instruction in the 14-n-th \ macro, as an offset from byte2 \ \ This effectively takes the end point given in the \ staDrawByteTyre table and returns the start point if \ the range DRAW_BYTE 26 to DRAW_BYTE 39 were \ "reflected" into DRAW_BYTE 39 to DRAW_BYTE 26 \ \ This enables us to calculate the offset of the start \ point's macro for the left tyre, as an offset from \ DrawTrackBytes, which is what we want STA view13+1 \ Modify the instruction at view13 to change the low \ byte of the address to A, so the instruction at view13 \ changes so it jumps to the LDA #0 instruction in the \ DRAW_BYTE macro specified in the table LDY tyreEdgeIndex,X \ Set Y to the index of the mask and pixel bytes for the \ tyre edge for this track line, so we can use it below \ to fetch the correct entries from leftTyreMask and \ leftTyrePixels LDA tyreRightEdge,X \ Fetch the track pixel byte that would be shown along \ the right edge of the left tyre, i.e. the leftmost \ byte of the track line, where the line meets the left \ tyre AND leftTyreMask,Y \ We now merge the track byte in A with the edge of the ORA leftTyrePixels,Y \ left tyre, by masking out the pixels in A that are \ hidden by the tyre (with AND leftTyreMask), and \ replacing them with the pixels from the edge of the \ left tyre (with ORA leftTyrePixels) TAY \ Copy the pixel byte into Y, because the following JSR \ jumps straight to the LDA #0 instruction within the \ DRAW_BYTE macro, and at that point the macro expects \ the pixel byte to be in Y rather than A .view13 JSR DrawTrackBytes \ Draw the left portion of this track line \ \ This routine was modified above to return from the \ subroutine at the STA instruction in the DRAW_BYTE \ macro specified in the staDrawByte table, so this \ returns the last pixel byte of this portion of the \ line in A, i.e. the rightmost byte of the left portion \ of the track line, where the line meets the left \ border of the central part of the dashboard AND leftDashMask,X \ We now merge the track byte in A with the left edge ORA leftDashPixels,X \ of the dashboard, by masking out the pixels in A that \ are hidden by the dashboard (with AND leftDashMask), \ and replacing them with the pixels from the left edge \ of the dashboard (with ORA leftDashPixels) STA (P),Y \ Write the merged pixel byte into screen memory LDY staDrawByteTyre,X \ Set Y to the X-th entry in staDrawByteTyre, which \ contains the low byte of the address of the STA (P),Y \ instruction in the DRAW_BYTE macro given in the \ table CPY view14+1 \ If the instruction at view14 has already been modified BEQ view16 \ to this address, jump to view16 to skip the following \ modifications, as they have already been done on the \ previous iteration of the loop LDA #&91 \ Set A to the opcode for the STA (P),Y instruction .view14 STA byte2+15 \ Modify the specified instruction back to STA (P),Y \ (the address of the modified instruction is set by the \ following, so the first time we run this line it has \ no effect) STY view15+1 \ Modify the instruction at view15 to change the low \ byte of the address to the X-th entry in \ staDrawByteTyre, so the instruction at view15 changes \ the STA (P),Y instruction to an RTS in the DRAW_BYTE \ macro given in staDrawByteTyre STY view14+1 \ Modify the instruction at view14 to change the low \ byte of the address to the X-th entry in \ staDrawByteTyre, so the instruction at view14 changes \ the STA (P),Y instruction to an RTS in the DRAW_BYTE \ macro given in staDrawByteTyre LDA #&60 \ Set A to the opcode for the RTS instruction .view15 STA byte2 \ Modify the specified instruction to an RTS so the next \ call to byte2 will return at that point (the address \ of the modified instruction is set above) .view16 LDY ldaDrawByte,X \ Set Y to the X-th entry in ldaDrawByte, which contains \ the low byte of the LDA #0 instruction in the specific \ DRAW_BYTE macro, as given in the table STY view17+1 \ Modify the instruction at view17 to change the low \ byte of the address to the X-th entry in ldaDrawByte, \ so the instruction at view17 changes so it jumps to \ the LDA #0 instruction in the DRAW_BYTE macro \ specified in the table LDA dashRightEdge,X \ Fetch the track pixel byte that would be shown along \ the right edge of the dashboard, i.e. the leftmost \ byte of the right portion of the track line, where the \ line meets the right border of the central part of the \ dashboard AND rightDashMask,X \ We now merge the track byte in A with the right edge ORA rightDashPixels,X \ of the dashboard, by masking out the pixels in A that \ are hidden by the dashboard (with AND rightDashMask), \ and replacing them with the pixels from the left edge \ of the dashboard (with ORA rightDashPixels) TAY \ Copy the pixel byte into Y, because the following JSR \ jumps straight to the LDA #0 instruction within the \ DRAW_BYTE macro, and at that point the macro expects \ the pixel byte to be in Y rather than A .view17 JSR byte2 \ Draw the right portion of this track line \ \ This JSR was modified above to jump to the LDA #0 \ instruction in the DRAW_BYTE macro specified in the \ ldaDrawByte table STY U \ Store Y in U so we can retrieve it below LDY tyreEdgeIndex,X \ Set Y to the index of the mask and pixel bytes for the \ tyre edge for this track line, so we can use it to \ fetch the correct entries from rightTyreMask and \ rightTyrePixels AND rightTyreMask,Y \ We now merge the track byte in A with the edge of the ORA rightTyrePixels,Y \ right tyre, by masking out the pixels in A that are \ hidden by the tyre (with AND rightTyreMask), and \ replacing them with the pixels from the edge of the \ right tyre (with ORA rightTyrePixels) LDY U \ Retrieve the value of Y that we stored above STA (R),Y \ Write the merged pixel byte into screen memory, using \ (S R) as the screen address as this is at the right \ end of the track line CPX #3 \ If we just drew the line at dash data entry 3, jump BEQ view18 \ to view18 to stop drawing track lines JMP view7 \ Loop back to keep drawing lines, working our way down \ through the dash data from entry 27 down to entry 3 .view18 JMP view19 \ Jump to part 4 to reverse our code modifications \ ****************************************************************************** \ \ Name: DrawCarInMirror \ Type: Subroutine \ Category: Dashboard \ Summary: Draw a car in a specified segment of one of the wing mirrors, or \ clear a specified segment \ Deep dive: Wing mirrors \ \ ------------------------------------------------------------------------------ \ \ Arguments: \ \ Y Mirror segment (0 to 5) \ \ * 0 = left mirror, outer segment \ * 1 = left mirror, middle segment \ * 2 = left mirror, inner segment \ * 3 = right mirror, inner segment \ * 4 = right mirror, middle segment \ * 5 = right mirror, outer segment \ \ N Start offset within the segment for the car lines \ \ A End offset within the segment for the car lines (or 0 to \ clear the mirror segment) \ \ ------------------------------------------------------------------------------ \ \ Returns: \ \ Y Y is unchanged \ \ ****************************************************************************** .DrawCarInMirror STA RR \ Store A in RR STY G \ Store Y in G so we can retrieve it before returning \ from the subroutine LDA mirrorAddressHi,Y \ Set (Q P) to the base screen address of this mirror STA Q \ segment (to which we add the following offsets to get LDA mirrorAddressLo,Y \ the screen address for this particular segment) STA P LDA startMirror,Y \ Set W to the offset of the first pixel byte in this STA W \ mirror segment LDA endMirror,Y \ Set Y to the offset of the first pixel byte in this TAY \ mirror segment .mirr1 \ We now work our way through the mirror segment pixel \ bytes, going backwards from the end byte to the start \ byte, either removing the car or drawing the car with \ added random blurriness LDA #%11110000 \ Set A to the pixel byte containing four pixels of \ colour 2 (white) CPY RR \ If Y >= RR, jump to mirr2 to draw a white pixel byte BCS mirr2 CPY N \ If Y < N, jump to mirr2 to draw a white pixel byte BCC mirr2 \ If we get here then N <= Y < RR, so we draw a pixel \ byte of black pixels to represent the car, with the \ pixels randomised but tending to black when the \ engine is on (if the engine is off, then all the \ pixels are black, so this part simulates the mirror \ shuddering when the engine is on) LDX 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 &2000,X \ There is game code at location &2000, so this randomly \ switches some of the white pixels (colour 2) to black \ (colour 0) in the pixel byte in A AND engineStatus \ If our engine is off, then engineStatus is zero and \ all the pixels are set to black, but if the engine is \ on, engineStatus is &FF so this instruction has no \ effect, leaving the image randomised .mirr2 STA (P),Y \ Draw the pixel byte in A at screen address (Q P) + Y DEY \ Decrement Y to point to the pixel byte above BPL mirr3 \ If bit 7 of Y is clear, jump to mirr3 to move on to \ the next pixel byte TYA \ If Y mod 8 < 7 then jump to mirr3 to move on to the AND #7 \ next pixel byte CMP #7 BCC mirr3 \ If we get here then Y has bit 7 set and Y mod 8 = 7, \ so we have gone past the top of the current character \ row and need to move up to the last pixel row in the \ character block above row instead \ \ There are &140 bytes per row, and we want to move from \ the top of the character block on this row to the \ bottom of the character block on the row above, so we \ subtract &140 to go up to the top of the character \ block on the row above, and then add 8 to jump down to \ the bottom row, i.e. we subtract &140 - 8 = &138 LDA P \ Set (Q P) = (Q P) - &138 SEC \ SBC #&38 \ starting with the low bytes STA P LDA Q \ And then the high bytes, so (Q P) points to the end of SBC #&01 \ the character block on the previous character row STA Q \ (i.e. the next pixel row up) .mirr3 CPY W \ Loop back to draw the next pixel byte, until Y < W BCS mirr1 LDY G \ Retrieve the value of Y that we stored in G, so that \ Y is preserved through the call to the routine RTS \ Return from the subroutine EQUB &FF \ This byte appears to be unused \ ****************************************************************************** \ \ Save Revs.bin \ \ For an explanation of the following, see the deep dive on "The jigsaw puzzle \ binary" \ \ ****************************************************************************** ORG &9000 INCBIN "1-source-files/images/dashboard.bin" COPYBLOCK &9EF6, &9EF6+10, dashData25 \ Step 1: Insert the dashboard image COPYBLOCK &9ECD, &9EF6, dashData26 \ into the game code, split into 18 COPYBLOCK &9E99, &9ECD, dashData27 \ pieces COPYBLOCK &9E61, &9E99, dashData28 COPYBLOCK &9E25, &9E61, dashData29 COPYBLOCK &9DE5, &9E25, dashData30 COPYBLOCK &9DA1, &9DE5, dashData31 COPYBLOCK &9D58, &9DA1, dashData32 COPYBLOCK &9D0B, &9D58, dashData33 COPYBLOCK &9CBE, &9D0B, dashData34 COPYBLOCK &9C72, &9CBE, dashData35 COPYBLOCK &9C38, &9C72, dashData36 COPYBLOCK &9C04, &9C38, dashData37 COPYBLOCK &9BD0, &9C04, dashData38 COPYBLOCK &9B9C, &9BD0, dashData39 COPYBLOCK &9B68, &9B9C, dashData40 COPYBLOCK &9B25, &9B68, dashData41 COPYBLOCK &9000, &9B25, dashData42 COPYBLOCK &7FCC, &8000, dashData0 \ Step 2: Insert the code that runs COPYBLOCK &7F98, &7FCC, dashData1 \ in screen memory into the game COPYBLOCK &7F64, &7F98, dashData2 \ code, split into 26 pieces COPYBLOCK &7F2A, &7F64, dashData3 COPYBLOCK &7EDE, &7F2A, dashData4 COPYBLOCK &7E91, &7EDE, dashData5 COPYBLOCK &7E44, &7E91, dashData6 COPYBLOCK &7DFB, &7E44, dashData7 COPYBLOCK &7DB7, &7DFB, dashData8 COPYBLOCK &7D77, &7DB7, dashData9 COPYBLOCK &7D3B, &7D77, dashData10 COPYBLOCK &7D03, &7D3B, dashData11 COPYBLOCK &7CCF, &7D03, dashData12 COPYBLOCK &7CA6, &7CCF, dashData13 COPYBLOCK &7C82, &7CA6, dashData14 COPYBLOCK &7C5E, &7C82, dashData15 COPYBLOCK &7C3A, &7C5E, dashData16 COPYBLOCK &7C16, &7C3A, dashData17 COPYBLOCK &7BF2, &7C16, dashData18 COPYBLOCK &7BCE, &7BF2, dashData19 COPYBLOCK &7BAA, &7BCE, dashData20 COPYBLOCK &7B86, &7BAA, dashData21 COPYBLOCK &7B62, &7B86, dashData22 COPYBLOCK &7B3E, &7B62, dashData23 COPYBLOCK &7B1A, &7B3E, dashData24 COPYBLOCK &7AF6+10, &7B1A, dashData25+10 COPYBLOCK &5FD0, &6700, &64D0 \ 3: Split the game code into the COPYBLOCK &0D00, &16DC, &5A80 \ parts that make up the game binary COPYBLOCK &7000, &70DB, &1500 \ file and pack them together in the COPYBLOCK &70DB, &7725, &5300 \ correct order COPYBLOCK &0B00, &0D00, &1300 COPYBLOCK &7900, &7A00, &1200 CLEAR &645C, &64D0 ORG &15DB CLEAR &15DB, &16DC EQUB &20, &00 \ 4: Add workspace noise to match EQUB &63, &60 \ the final game binary EQUB &A6, &03 EQUB &10, &03 EQUB &20, &CB EQUB &2A, &20 EQUB &84, &50 EQUB &E4, &4D EQUB &D0, &F6 EQUB &A2, &16 EQUB &86, &45 EQUB &20, &D1 EQUB &2A, &CA EQUB &E0, &14 EQUB &B0, &F6 EQUB &A6, &4D EQUB &20, &CB EQUB &2A, &60 EQUB &20, &0E EQUB &2B, &A2 EQUB &F4, &20 EQUB &CC, &0B EQUB &20, &0E EQUB &2B, &A2 EQUB &FD, &20 EQUB &CC, &0B EQUB &A9, &14 EQUB &85, &42 EQUB &A9, &02 EQUB &20, &5D EQUB &2A, &A9 EQUB &15, &85 EQUB &42, &A9 EQUB &01, &A2 EQUB &F4, &20 EQUB &5F, &2A EQUB &A9, &16 EQUB &85, &42 EQUB &A9, &00 EQUB &A2, &FA EQUB &20, &5F EQUB &2A, &A6 EQUB &45, &60 EQUB &C9, &05 EQUB &90, &F9 EQUB &BD, &8C EQUB &01, &30 EQUB &F4, &FE EQUB &8C, &01 EQUB &60, &A2 EQUB &FD, &85 EQUB &37, &20 EQUB &45, &21 EQUB &A4, &42 EQUB &A5, &8A EQUB &99, &80 EQUB &03, &A5 EQUB &8B, &99 EQUB &98, &03 EQUB &20, &B1 EQUB &2A, &20 EQUB &85, &22 EQUB &A4, &42 EQUB &B0, &2C EQUB &38, &E9 EQUB &01, &30 EQUB &27, &99 EQUB &B0, &03 EQUB &A5, &2B EQUB &38, &E9 EQUB &09, &AA EQUB &A5, &2A EQUB &CA, &F0 EQUB &0C, &10 EQUB &06, &4A EQUB &E8, &D0 EQUB &FC, &F0 EQUB &04, &0A EQUB &CA, &D0 EQUB &FC, &99 EQUB &C8, &03 EQUB &B9, &8C EQUB &01, &29 EQUB &70, &05 EQUB &37, &4C EQUB &AD, &2A EQUB &A4, &42 EQUB &B9, &8C EQUB &01, &09 EQUB &80, &99 EQUB &8C, &01 EQUB &60, &A0 EQUB &25, &20 EQUB &A5, &0C EQUB &A5, &7D EQUB &85, &55 EQUB &D0, &0E EQUB &C4, &7C EQUB &90, &0A EQUB &C6, &68 EQUB &A5, &7C EQUB &85, &41 EQUB &A5, &42 EQUB &85, &67 EQUB &60, &86 EQUB &45, &BD EQUB &3C, &01 EQUB &AA, &BD EQUB &8C, &01 EQUB &30, &35 EQUB &29, &0F EQUB &85, &37 EQUB &BD, &80 EQUB &03, &38 EQUB &E5, &0A EQUB &85, &74 EQUB &BD, &98 EQUB &03, &E5 EQUB &0B, &10 EQUB &06, &C9 EQUB &E0, &90 EQUB &1E, &B0 EQUB &04, &C9 EQUB &20, &B0 EQUB &18, &06 EQUB &74, &2A EQUB &06, &74 EQUB &2A, &18 EQUB &69 SAVE "3-assembled-output/Revs2.bin", LOAD%, LOAD_END%