Every byte in the Revs track data file, explained
Right from the start, Revs was built to support more than one track. The original 1985 release may have shipped with just one venue - the iconic Silverstone - but Geoff Crammond cleverly split out the track definition into its own file, separate from the main game binary, so at some point in the future more tracks could be designed and sold to owners of the original game.
This plan bore fruit later in the year, when the excitingly titled Revs 4 Tracks was released in time for Christmas, giving existing owners of the original Revs access to four brand new tracks: Brands Hatch, Donington Park, Oulton Park and Snetterton. This release didn't contain the Revs game itself, but consisted only of four new track data files, plus a menu program that loaded the chosen track before prompting for the original game disc so it could run the game code.
The data files for these four extra tracks differ from the original release's Silverstone track in a large number of ways; see the deep dive on secrets of the extra tracks for details. However, the core of the two file formats are the same, so most of this article applies to both formats.
Let's take a detailed look at the Silverstone track data file - the original and quintessential Revs track.
Structure of the track data file
--------------------------------
Let's start with a breakdown of every single byte in the Silverstone track data file, which is the file called "Silvers" on the original game disc. This file contains 1,849 bytes of data that initially loads at memory address &70DB, before the loader moves it down to &5300 (see the deep dive on the jigsaw puzzle binary for details of the loading process).
The track data in this file is read-only and is not changed in any way once it has been moved to &5300. All the data in the file is labelled with names that start with "track" (or, in the case of axis-based coordinates or vectors, with "xTrack", "yTrack" or "zTrack"). This prefix is only used for data from the track data file and is not used anywhere else in the source, so when you see the likes of trackSectionData, trackLength or xTrackSegmentI in the code, you can be sure they are referring to values in the read-only track data file.
Some points before we get to the breakdown:
- Each of the following bits of data is described in more detail below the table; click the links in the right column to jump down to a summary of all the data in that category.
- Although the data file format supports data for 26 sections, Silverstone only uses 24 of these (any data for the other two sections is simply ignored).
- For 3D coordinates, Revs uses a standard left-handed axis system, which is exactly the same as in Aviator and Elite. This means that when we are looking at the screen, the x-axis runs from left to right, the y-axis runs from down to up, and the z-axis points into the screen. In terms of the track's coordinates in the 3D world, the y-axis gives us the track elevation, while the x-axis is our longitude (west to east) and the z-axis is our latitude (south to north).
- We drive clockwise round Silverstone, with the right verge being on the inside of the track, and the left verge on the outside.
- For more information about sections, segments and the fundamental building blocks of the track data, see the deep dive on building a 3D track from sections and segments.
Now that's out of the way, here's the structure of the Revs track data file, all 1,849 bytes of it from &5300 to &5A38.
Name and address | Bytes | Description | Category |
---|---|---|---|
&5300 to &53CF | 8 * 26 | Track section data (Part 1 of 2): Eight bytes of data for each of the 26 track sections | |
&53D0 to &53DF | 1 * 16 | The x-coordinate of the track sign vector for each of the 16 road signs | |
&53E0 to &53EF | 1 * 16 | The z-coordinate of the track sign vector for each of the 16 road signs | |
&53F0 to &53FF | 1 * 16 | The y-coordinate of the track sign vector for each of the 16 road signs | |
&5400 to &54FF | 256 | Vector x-coordinates between two consecutive segments on the inside of the track | |
&5500 to &55FF | 256 | Vector y-coordinates between two consecutive segments on the inside of the track | |
&5600 to &56FF | 256 | Vector z-coordinates between two consecutive segments on the inside of the track | |
&5700 to &57FF | 256 | Vector x-coordinates from the inside to the outside of the track for each segment | |
&5800 to &58FF | 256 | Vector z-coordinates from the inside to the outside of the track for each segment | |
&5900 to &59CF | 8 * 26 | Track section data (Part 2 of 2): Eight bytes of data for each of the 26 track sections | |
&59D0 to &59E9 | 1 * 26 | The optimum racing line for non-player drivers on each of the 26 track sections | |
&59EA to &59F9 | 1 * 16 | Base coordinates and object types for each of the 16 road signs | |
&59FA | 1 | The total number of track sections * 8 | |
&59FB | 1 | The total number of segment vectors in the segment vector tables | |
&59FC to &59FD | 2 | The length of the full track in terms of segments | |
&59FE to &59FF | 2 | The segment number of the starting line, encoded as 1024 - n | |
&5A00 to &5A02 | 1 * 3 | Lap times for adjusting each of the three race classes (seconds) | |
&5A03 to &5A05 | 1 * 3 | Lap times for adjusting each of the three race classes (minutes) | |
&5A06 to &5A0C | 1 * 7 | Gear ratios for this track, one for each of the seven gears (R, N, 1 to 5) | |
&5A0D to &5A13 | 1 * 7 | Gear powers for this track, one for each of the seven gears (R, N, 1 to 5) | |
&5A14 to &5A16 | 1 * 3 | The base speed for each of the three race classes, used when generating the best racing lines and non-player driver speeds | |
&5A17 | 1 | The starting race position of the player during a qualifying lap | |
&5A18 | 1 | The spacing between the cars at the start of a qualifying lap, in segments | |
&5A19 | 1 | Adjustment factor for the speed of the timers to allow for fine-tuning of time on a per-track basis | |
&5A1A | 1 | Slowdown factor for non-player drivers in the race | |
Unused bytes &5A1A to &5A21 | 7 | - | - |
&5A22 to &5A24 | 3 | The track file's hook code | |
&5A25 to &5A38 | 20 | The track file's checksum and track name |
That's the format of the entire file, so now let's take a look at each of these entries in more detail, grouped together by type.
Track section data
------------------
The track in Revs is broken down into a number of sections, and those sections are broken down further into segments; we'll take a look at the section data here, and examine the segment data in the next part. See the deep dive on building a 3D track from sections and segments for more details about how the track is built from these two fundamental components.
Each track section is defined by the following data in the track data file:
- The 3D coordinates of the start of the section, with one 16-bit coordinate for the inner track verge and another one for the outer verge
- The shape of the section (straight or curved)
- The size of the section, in terms of segments
- Whether the section contains corner markers, and if so, their colours (all white, or white and red)
- The colour of the track verges on either side of the section (black-and-white or red-and-white)
- Data that controls the behaviour of non-player drivers on this section
The track data file format supports up to 26 track sections, numbered from 0 to 25 (though Silverstone only uses 24 of these, from 0 to 23). Here's a breakdown of the section-related data in the track data file.
- trackSectionCount defines the number of sections in the track. This is encoded as the section count multiplied by 8 (so for Silverstone, it contains 24 * 8 = 192). The number is multiplied by 8 so it can be used as an index into the two blocks of section data at trackSectionData and trackSectionFlag.
- trackSectionData is the first of two section data blocks. It contains eight bytes of configuration data for each section, so the whole block contains a total of 26 * 8 = 208 bytes. We'll look at this block in detail below.
- trackSectionFlag is the second of the section data blocks. Again it contains 208 bytes, and as with the first block, each section has eight bytes, giving a total of 16 bytes of data spread across the two blocks.
Let's look at the two section data blocks in detail.
The first block is at trackSectionData, right at the start of the track data file at address &5300, so the data for section 0 is from &5300 to &5307, the data for section 1 is from &5308 to &530F, and so on, up to the data for section 25 from &53C8 to &53CF. Here's the breakdown of this first block of section data; the addresses shown are those for section 0, with the data for section n being at &5300 + (n * 8).
Name | Description |
---|---|
trackSectionData &5300 |
|
xTrackSectionIHi &5301 | High byte of the x-coordinate of the starting point of the inner verge of each track section |
yTrackSectionIHi &5302 | High byte of the y-coordinate of the starting point of the inner verge of each track section |
zTrackSectionIHi &5303 | High byte of the z-coordinate of the starting point of the inner verge of each track section |
xTrackSectionOHi &5304 | High byte of the x-coordinate of the starting point of the outside verge of each track section |
trackSectionTurn &5305 | The number of the segment towards the end of the section where non-player cars should start turning in preparation for the next section |
zTrackSectionOHi &5306 | High byte of the z-coordinate of the starting point of the outside verge of each track section |
trackDriverSpeed &5307 | The maximum speed for non-player drivers on the next section of the track |
The second block is at trackSectionFlag, somewhat later in the track data file at address &5900, so the data for section 0 is from &5900 to &5907, the data for section 1 is from &5908 to &590F, and so on, up to the data for section 25 from &59C8 to &59CF. Here's the breakdown of this second block of section data; again, the addresses shown are those for section 0, with the data for section n being at &5900 + (n * 8).
Name | Description |
---|---|
trackSectionFlag &5900 |
|
xTrackSectionILo &5901 | Low byte of the x-coordinate of the starting point of the inner verge of each track section |
yTrackSectionILo &5902 | Low byte of the y-coordinate of the starting point of the inner verge of each track section |
zTrackSectionILo &5903 | Low byte of the z-coordinate of the starting point of the inner verge of each track section |
xTrackSectionOLo &5904 | Low byte of the x-coordinate of the starting point of the outside verge of each track section |
trackSectionFrom &5905 | The number of the first segment vector in each section, which enables us to fetch the segment vectors for a given track section |
zTrackSectionOLo &5906 | Low byte of the z-coordinate of the starting point of the outside verge of each track section |
trackSectionSize &5907 | The length of each track section in terms of segments |
The 3D coordinates for the start of each track section are stored as 16-bit signed integers in the two section data tables. There's one coordinate for the inner track verge and another for the outer verge. Specifically, the coordinates for each section are stored as follows:
[ (xTrackSectionIHi xTrackSectionILo) ] Inner verge starts at [ (yTrackSectionIHi yTrackSectionILo) ] [ (zTrackSectionIHi zTrackSectionILo) ] [ (xTrackSectionOHi xTrackSectionOLo) ] Outer verge starts at [ (yTrackSectionIHi yTrackSectionILo) ] [ (zTrackSectionOHi zTrackSectionOLo) ]
The inner and outer verges are always at the same elevation at the start of each section (as well as within each individual segment), so although the track goes up and down as we drive through different sections and segments, it remains level in terms of camber, i.e. when going from one side of the track to the other. You can see this in the above, as the section's inner and outer coordinates share the same y-coordinate (the y-axis being the axis of elevation, going up and down the screen). See the deep dive on building a 3D track from sections and segments for a deeper look into this process.
Now that we have defined the track sections, let's move on to the track segments.
Track segment data
------------------
Each track section is broken down into a number of segments. You can see these segments as you drive around the track, as each segment corresponds to a single coloured strip along the track verge (so each black, white or red strip represents exactly one segment). See the deep dive on building a 3D track from sections and segments for more details about how the track is built from these two fundamental components.
Each track segment is defined by the following data in the track data file:
- The inner segment vector, which contains the vector from the previous segment to this one
- The outer segment vector, which contains the vector from the inner verge of the segment to the outer verge
- The index of the first segment vector in each section
- The number of segments in each section
Here's a breakdown of the segment-related data in the track data file.
- trackSectionSize in the second section data block (see above) defines the number of segments in each section. Each section also has a set of segment vectors associated with it; straight sections have just one segment vector (we'll see why in a minute), while curved sections have one segment vector for each segment in the curve (so each curved section has trackSectionSize segment vectors).
- trackSectionFrom, also in the second section data block (see above), contains the number of the first segment vector in each section, as in index into the inner and outer segment vector tables. This enables us to fetch the segment vectors for a given track section. (Incidentally, for curved sections, trackSectionFrom + trackSectionSize gives us the trackSectionFrom value for the next section; for straight sections, the next section's trackSectionFrom value is trackSectionFrom + 1, as straight sections only have one segment vector.)
- xTrackSegmentI, yTrackSegmentI and zTrackSegmentI contain the inner segment vector for each segment, with each coordinate being a signed 8-bit value:
[ xTrackSegmentI ] Inner segment vector = [ yTrackSegmentI ] [ zTrackSegmentI ]
The inner segment vector is the vector along the inner verge of the track, from the previous segment to the current one. - xTrackSegmentO and zTrackSegmentO contain the outer segment vector for each segment, with each coordinate being a signed 8-bit value:
[ xTrackSegmentO ] Outer segment vector = [ yTrackSegmentI ] [ zTrackSegmentO ]
The outer segment vector is the vector from the inner segment to the outer segment (i.e. across the track). As the track is level from the inner to the outer verge, the outer segment vector can reuse the y-coordinate from the inner vector in yTrackSegmentI. - trackVectorCount contains the total number of segment vectors in the data file, which is 255 for the Silverstone track. As 255 is the maximum number you can store in one byte, and the segment vector tables at xTrackSegmentI, yTrackSegmentI, zTrackSegmentI, xTrackSegmentO and zTrackSegmentO contain 256 entries, this means the last coordinate in each table is always unused.
To calculate the 3D coordinates for a specific track segment in a curved section, we take the start coordinates for the section, and add all the segment vectors in order, until we reach the coordinate for the segment we're after. Then, to get the outer coordinate, we add that segment's outer segment vector. See the deep dive on building a 3D track from sections and segments for a deeper look into this process.
(Note that in the extra track files, the segment vectors are dynamically generated rather than being hard-coded, though the concept of the segment vector is unchanged; this is the main difference between the standard track data file format and the extra tracks file format. See the deep dive on dynamic track generation in the extra tracks for details.)
Now that we have defined the two fundamental track building blocks, let's move on to the rest of the data.
Road sign data
--------------
Each track in Revs can support up to 16 road signs by the side of the track. The track data file contains data for the 3D coordinates of each sign, as well as the type of sign and an associated track section number. See the deep dive on road signs for more details about how the following data is used to display the road signs.
Here's a breakdown of the sign-related data in the track data file.
- trackSignData contains one byte for each of the 16 signs, containing the following data:
- Bits 0-2 = The object type of the road sign (we add 7 to get the type). The different types of road sign are described in the deep dive on object definitions.
- Bits 3-7 = The number of the track section to use as the base coordinates for the sign (to get the sign's 3D coordinates, we add the scaled track sign vector to this section's start coordinates). Note that this simply defines each sign's 3D coordinates; the mapping between track sections and the signs within those sections is defined elsewhere, in each section's trackSectionData byte.
- xTrackSignVector, yTrackSignVector and zTrackSignVector contain the track sign vector for each sign, as a vector of 8-bit signed integers. To get the 3D coordinates of the sign, we take the coordinates of the track section whose number is in the trackSignData table (see the previous point), and add the track sign vector, scaled as follows:
[ (xTrackSectionIHi xTrackSectionILo) ] [ xTrackSignVector * 2 ^ 6 ] [ (yTrackSectionIHi yTrackSectionILo) ] + [ yTrackSignVector * 2 ^ 4 ] [ (zTrackSectionIHi zTrackSectionILo) ] [ zTrackSignVector * 2 ^ 6 ]
This gives us the 3D coordinate of the sign, which we can use to draw the sign on-screen and check for collisions. - trackSectionData in the first block of track section data (see above) contains a sign number for each section, in bits 4-7 (the high nibble, i.e. the first digit in the hexadecimal value). This defines when we show each sign, and the logic is that a sign is shown when we move into a new section whose sign number differs from the previous section's sign number.
Track layout data
-----------------
The track data file contains various bits of data to do with the track layout.
- trackLength contains the total number of segments in the full track, which is 1024 for Silverstone. Note that this counts all the segments along the whole track, though in terms of segment data in the track data file, we only store coordinates for trackVectorCount segments out of this total (which is 255 for Silverstone).
The reason we can have 1024 track segments with only 255 segment vectors is the way straight track sections are stored, as these only require one segment vector but can contain large numbers of segments (the biggest section at Silverstone is section 15, which contains 135 segments and forms the main part of Hangar Straight, which therefore only needs one segment vector in the data file).
Each car's position on the track is stored as a segment number in (objectSegmentHi objectSegmentLo), ranging from 0 to trackLength, and with the starting line being segment 0. Note that segment 0 represents the starting line, but this is not the same as the first segment in of section 0; at Silverstone, the starting line is partway through section 4, and therefore so is segment 0. - trackStartLine is the position of the starting line, expressed as the number of segments from the starting line to the start of section 0, counting forwards around the track. If the starting line is n segments from the start of section 0, this value is the track length minus n; for Silverstone, the starting line is 181 segments after the start of section 0, so this value is store as 1024 - 181 in the data. Encoding the value in this manner simplifies the code to space out the cars backwards from the starting line, which you can see in the PlaceCarsOnTrack routine.
- trackCarSpacing is the number of segments inserted between each car when placing the cars on the track during qualifying. This is set to 40 for Silverstone, so the driver in first place is placed 40 segments before the starting line, then the second driver is placed 40 segments before that (i.e. 80 segments before the starting line), the third driver is placed 40 segments before that (i.e. 120 segments before the starting line), and so on. During races, this value is ignored and cars are spaced out by one segment on the grid.
- trackStartPosition is the starting race position of the player during a practice or qualifying lap. When the cars are placed on the track and spaced out by the number of segments given in trackCarSpacing, the player car is placed at this position. For Silverstone this value is 4, and it counts from zero, so the player's car is the fifth car on the track. As the cars are spaced out by 40 segments as described in the previous point, with the first car 40 segments behind the starting line, this means that for qualifying (and practice), the player's car starts 200 segments back from the starting line, which means we start practice laps partway through section 23 (the last section).
Driver-related data
-------------------
The track data file contains a number of driver-related values.
- trackSteering is used to calculate the optimum steering for non-player cars to apply through each section. See the deep dive on tactics of the non-player drivers for details.
- trackLapTimeMin and trackLapTimeSec as used to balance the game for players who may be struggling. If the player's qualifying laps are too slow for the chosen race class, then the game will automatically switch to an easier class. The point at which the race is switched to an easier class is defined by the values of trackLapTimeMin and trackLapTimeSec. For Silverstone, the class is switched to Novice if the slowest human lap time is greater than 1:51, or it's switched to Amateur if the slowest lap time is greater than 1:41.
- trackGearRatio and trackGearPower define the gear ratios and power for the track. These values govern the relationship between engine revs and tyre speed, and the transfer of power from the engine to the tyres via the gearbox (see the deep dive on modelling the engine for details).
- trackBaseSpeed defines the base speed for each race class, which is used when generating the optimum racing lines and the speeds of individual non-player drivers.
- trackTimerAdjust is an adjustment factor for the speed of the timers to allow for fine-tuning of time on a per-track basis. The value of the timerAdjust variable in the main game code is incremented on every iteration of the main driving loop. When it reaches the value in trackTimerAdjust, the timers adds 18/100 of a second rather than 9/100 of a second. Increasing this value therefore speeds up the passage of time, allowing the timer speed to be adjusted on a per-track basis.
So if a track is particularly heavy on the processor, you would decrease this setting, or for simpler tracks you would increase it. In both cases, the aim is to make one second of the timer match one second in the outside world. Setting this value to 255 disables the timer adjustment, and it is set to 24 for Silverstone, so the timer ticks twice as fast, once every 24 iterations around the main driving loop. - trackRaceSlowdown reduces the speed of all cars in a race by this amount (this does not affect the speed during qualifying). I suspect this is used for testing purposes, as it is set to 0 for Silverstone.
Code-related data
-----------------
Finally, there are two code-related bits of data in the track data file, two of which are absolutely essential features of the track file system.
- CallTrackHook consists of just three bytes, which in the Silverstone track contain the following assembly instructions:
RTS NOP NOP
When the track file is loaded and the SetupGame routine has finished running, the game runs a JSR CallTrackHook instruction that calls this code in the track data file. Silverstone simply returns straight away, but the tracks in Revs 4 Tracks contain a further JSR call in this location, which jumps to code that's buried elsewhere in the track data file. This code modifies the main game code in-place to support different data formats and features. These three bytes might be simple, but they enable the track data file format to be extendable and future-proofed. Adding this hook was a really clever move from Geoff Crammond, and its usage is fully explored in the deep dive on secrets of the extra tracks. - trackChecksum contains four checksum bytes for the track data file. These are checked in the SwapCode routine, where the game crashes if they do not match, clearing the game code from memory in the process. This prevents casual players from simply hacking the track data file to make things easier, though the checksum algorithm is pretty basic, simply counting the number of bytes in the data file as follows (where the data bytes run from the start of the file at &5300 to the end of CallTrackHook at &5A24):
- 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
- trackGameName contains the game name ("REVS") and the track name ("Silverstone") as strings. The game name is checked during the loading process to see whether a track file has been loaded (and if not, one is loaded). This data is not copied into main memory, as it is only used during the loading process.
- trackName contains the track name (e.g. "Silverstone"), terminated by a carriage return. This is displayed in the game's mode 7 loading screen. This data is not copied into main memory, as it is only used during the loading process.
So that's the Revs track data file format. For more details of how the above data is used, see the deep dives on building a 3D track from sections and segments, road signs, corner markers and the track verges, amongst others. Also, see the deep dives on secrets of the extra tracks, dynamic track generation in the extra tracks and the extra tracks data file format for details of the differences in the extra track files.
My hope is that one day, this information will allow enterprising developers to create brand new track files for Revs. Wouldn't that be something...