How the extra track files modify the main game code using code hooks
Revs might have an astonishing physics engine, super-smooth animation, and a genuine claim to be the first real racing simulation, but one of the cleverest moments in the source code is just three bytes long. Without these three bytes, this menu would simply not be possible:
So what is this witchcraft? Well, buried amongst the coordinate and vector data in the track data file for Silverstone - the only track in the original 1985 release of Acornsoft Revs - is this sequence of instructions:
RTS NOP NOP
This is the only bit of code in the whole file; everything else is track data. These three instructions form the CallTrackHook routine, and 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.
In Silverstone, this has no effect; the first instruction is an RTS, so it simply returns straight away and the game continues. But this three-byte sequence is crucial to making Revs more than just a one-track pony; it enables the Revs 4 Tracks expansion pack to extend the capabilities of the original game by modifying the game binary during the track-loading process. The extra tracks therefore contain more curves, sharper hairpins and much steeper hills than in the Silverstone release, and all this is achieved without needing to release a new version of the game code.
Some of the extra tracks even use this hook to update the game's assets. For example, in Brands Hatch, the Nürburgring and Oulton Park, the track data file uses the track hook to modify the chicane road sign into a hairpin road sign, like this:
This is only scratching the surface of this complex system of modifications, so let's see exactly what the extra tracks get up to when the track hook is called.
The hook system in the extra tracks
If we look at the contents of the CallTrackHook routine in the extra tracks, then instead of the RTS and two NOPs, we have a three-byte jump instruction:
The CallTrackHook routine is exactly the same in all the extra tracks, but as both CallTrackHook and ModifyGameCode are part of the extra track file, the latter can be tailored to the individual needs of the track. The effect of the routine is the same in all cases - as the name implies, the call to the ModifyGameCode routine modifies the main game's code in a number of ways, before returning control back to the main game code to start the game.
What's surprising is the sheer number of modifications that the extra tracks apply to the main game code. I've listed them all in the deep dive on code hooks in the extra tracks, and in all there are 31 different modifications and three subsequent self-modifications that are applied by the extra tracks (though not all tracks need all of these modifications - only some tracks need a hairpin road sign, for example).
We'll look at the modifications in more detail in the next section, but first let's take a quick look at how the ModifyGameCode routine works. The routine is split into at least three parts, spread throughout the track file, and these parts are executed in sequence once the track file has finished loading.
There are three types of modification that are applied by the ModifyGameCode routine. They are all implemented by poking new values directly into the main game code, as follows:
- 8-bit (one-byte) pokes are done with straightforward LDA and STA instructions.
- Most 16-bit (two-byte) pokes take values from (newContentHi newContentLo) and poke the new 16-bit content into the game code addresses given in (modifyAddressHi modifyAddressLo).
- For tracks that need more 16-bit pokes than can be supported by the newContent and modifyAddress tables, additional 16-bit pokes are done with pairs of LDA and STA instructions.
For details of the modification process, see the ModifyGameCode routines in the individual tracks: Brands Hatch, Donington Park, the Nürburgring, Oulton Park and Snetterton. Note that the routine is in multiple parts, with some tracks requiring three parts, and some four parts.
In all cases, once the modifications are done and control is passed back to the main game code via an RTS, the space that was taken up by the various parts of the ModifyGameCode routine - and the associated newContent and modifyAddress tables - is released and reused for storing the dynamically generated segment vectors (see the deep dive on dynamic track generation in the extra tracks for details).
This is why the modification routine and its associated data are spread across the track file; all the various parts fit into the first 40 bytes of the segment vector tables, so once the game code has been modified, the space that the modification routines and tables occupies can be reused to store the dynamically generated segment vectors, while using the same addresses for the segment vectors as the Silverstone track. The only difference is that in Silverstone, the segment vectors are static data, while in the extra tracks, the vectors are dynamically generated into the space left by the modification routines. See the deep dive on the extra tracks data file format to see how this works within the structure of the extra track files.
The different types of modification
There are lots of modifications applied by the extra track files. These are listed in detail in the deep dive on code hooks in the extra tracks, but here's an overview of what's applied, and why.
In the following, unless otherwise noted, modifications apply to all five extra track files, and the links will take you to detailed descriptions of each modification.
The first type of modification we'll look at is the "hook routine". This modification injects a JSR instruction into the main game code, replacing existing instructions in the process. The destination of this JSR is a routine within the extra track file, so this modification effectively sticks a "hook" into the existing code, sending program execution through the new routine before returning to the original code. These hook routines have names that start with "Hook". Most of the time they include the instructions that the modification poke overwrites, though sometimes they don't.
The complete list of hook routines is as follows:
- HookSectionFrom, HookFirstSegment, HookDataPointers and HookSegmentVector implement dynamic segment vector generation, as described in the deep dive on dynamic track generation in the extra tracks
- HookJoystick is individually tailored for each track, and applies power steering and drift control to sharper bends, but only for joystick players (without the modifications, it isn't possible to steer around hairpin corners using the joystick)
- HookFlattenHills, HookFixHorizon, Hook80Percent and HookUpdateHorizon extend the horizon functionality to cope with higher hills and deeper valleys
- HookFieldOfView patches the verge buffer routine to prevent segments from disappearing when we're driving around sharp corners (as otherwise segments around hairpin corners are deemed outside the field of view and get hidden when they shouldn't)
- HookForward (Donington Park), HookMoveBack and HookFlipAbsolute update the player-moving routines so the player can travel over more segments in each iteration, to support both smaller segment sizes and faster car speeds
- HookBackground (Donington Park and Snetterton) fixes an issue with the background colour
- HookSlopeJump makes the car jump when driving fast over sloping segments
As well as inserting the hooks for the above routines, the extra tracks also make the following modifications:
- Poking to address &1310 manually sets the halfway segment number for two of the extra tracks (Donington Park and Snetterton)
- Poking to address &1FE9 modifies an instruction in the DrawObject routine to allow for self-modification of the routine, to support cutting off objects at the newly enhanced horizon
- Poking to address &231B avoids skipping entry number sectionListPointer when updating the entries in the track section list (Donington Park and Snetterton)
- Poking to address &24EA moves the player back by one or two segments, depending on the value of edgeSegmentNumber
- Poking to address &2772 makes cars apply the brakes when they're within a distance of 75 of each other, rather than 60
- Poking to address &298E removes the height restriction on segment y-coordinates, so the track elevation is no longer capped to a maximum of 8,192 (the Nürburgring)
- Poking to addresses &3574 and &35F4 changes the chicane road sign into a hairpin bend (Brands Hatch, the Nürburgring and Oulton Park)
- Poking to addresses &4F55 and &4F59 modifies the screen timer to allow the palette change to occur lower down the screen, to support higher hills
The final group of modifications updates the code to point to the new locations of the following track data, which are in different locations to the standard Silverstone track:
For more details of the specific modifications and the instructions that they replace, see the deep dive on code hooks in the extra tracks.
The extra tracks modification system is complex, so I've broken it down into a number of different topics. For more information about the modifications and the track data files, check out the following deep dives:
- Code hooks in the extra tracks - Documentation on every modification and code hook in the extra tracks
- Dynamic track generation in the extra tracks - How Revs squeezes complex track layouts into extremely small data files
- The extra tracks data file format - Differences between the standard Silverstone track and the extra track files
You might also find the following track maps and statistics useful when investigating the extra tracks, particularly if you're trying to shave seconds off your lap time:
- Comparing the tracks - How the six different tracks in Revs compare to each other
- The Brands Hatch track - One of the four extra tracks in the Revs 4 Tracks expansion
- The Donington Park track - One of the four extra tracks in the Revs 4 Tracks expansion
- The Nürburgring track - The last track to be released, as part of the Commodore 64 version of Revs+
- The Oulton Park track - One of the four extra tracks in the Revs 4 Tracks expansion
- The Silverstone track - The original track that came with the first release of Revs
- The Snetterton track - One of the four extra tracks in the Revs 4 Tracks expansion
Finally, as part of this project I have backported the Nürburgring track from the Commodore 64 release of Revs+ to the BBC Micro. If you would like to drive this track, see the deep dive on backporting the Nürburgring track.