The making of Don Matrelli’s Legacy
Part IV: putting it all together
In the past episodes we have seen how Grand Prix Circuit (GPC) was partly reverse-engineered, and how to build new tracks and cars. All the main components of my GPC mod have been described: for the final phases of the building process I’ll return to a more diaristic form.
The launch vehicle
Already from the start of the project I needed a practical method to inject the new data into the game: patching thememory via DOSBox debugger is only good for small experiments. I had not yet figured out how to decompress the executable, so I could not edit it. But I had an alternative plan: instead of modifying the GPEGA executable, my mod would live as separate TSR program to be run before the game. The TSR would install a callback in memory, and ask DOS to execute it whenever interrupt 10h (“Video BIOS Services”) was called. GPC calls this interrupt twice at game start: on the second invocation, after GPC had written its data in memory, the callback would replace them with the modded ones.
I loved this method: as an amateur DOS hacker, “create a TSR” and “hook an interrupt” were two achievements I wanted to add to my portfolio. I used an LLM to create the boilerplate, but the generated code needed so many manual fixes that I can claim at least partial points.
After some iterations, the basic TSR machinery started to work and successfully injected my prototypal track into the game. From there, it was easy to extend the code, adding the new assets as they were produced. Slowly I introduced further features:
- Use alternative names for all the modded files, so that the mod could coexist with the base game without overwriting any data on disk.
- Add command line options to selectively enable parts of the mod.
- Add a “failsafe” parameter to ensure the TSR was not started accidentally (ideally one should use the accompanying
.BATfile). - Increase the base difficulty for the legacy tracks.
TSRs and interrupt hooking are dangerous techniques; without an old machine to test on, I could only ensure that the mod worked in my DOSBox emulator. I’ve been informed that the game can be played on at least one real PC: absent other datapoints, this is good enough for me.
Qualifications
Part III has shown how to adjust the qualification times of the AI opponents. Surprisingly, this was the hardest piece of information to procure. I had tried to locate it already in the early phases of the project.
Q1
My first search attempt was simple. I supposed that GPC stores a “base qualification time” for each circuit and adds modifiers to create the effective times. I also made the assumption that the times:
- Are expressed in terms of tenth of seconds (matching the precision displayed on screen).
- Are represented as little-endian
int16. - Are similar to the effective ones (i.e. the modifiers are small), which means they lie between
0x300(1:16.8) and0x3FF(1:57.7), except for Monaco. - Are stored in a contiguous array.
If all of this holds, the base times would appear in the data segment in a pattern like this:
?? 03 ?? 03 ?? 03 ?? 03
I made an attempt to investigate the occurences of this pattern:
LC_ALL=C grep --text --byte-offset --only-matching -P '.\x03.\x03.\x03.\x03' memdump-2--ds-only.bin | awk -F':' '{ print $1 }' | xargs printf '%x\n'
but none of the matches looked like qualification times. Not surprising: any of my assumptions could have been wrong.
I gave up on the idea, but after my project grew from “minimod” to a full expansion, it felt like a shame to leave inconsistent qualification times. It was time for a renewed effort.
Q2
Since I had no new insights, I went for the most stolid route. Regardless of how a program works, at the end of the day all that it does is to paint pixels. Qualification times are nothing but pixels, too: let’s start from one and work the dataflow backwards.
The starting point was pixel (232, 66). Its “red” component is stored at the memory address A000:0A6D, the offset being floor((320y + x) / 8). I set a breakpoint there and began the hunt.
I had already tried this technique on Stunt Island, in vain. But now I had more hacking experience, better tools and much more evolved LLMs. All that was needed was discipline:
- Set a “memory changed” breakpoint on a byte.
- Use the
HEAVYLOGfunction of DOSBox to dump a log of the processor state in the 20,000 cycles preceeding the write. - Deduct from the log which was the input value of the function that altered that byte.
- Set a breakpoint on that value, and repeat.
Since DOSBox has no time-travel functionality, each iteration involved restarting the game, entering a new race and blowing the car’s engine to quickly abort the qualification1. The game then started to display the result sheet, triggering the breakpoint. Each iteration revealed a new piece of the dataflow:
- The pixel on screen is copied from a off-screen buffer.
- The byte in that buffer is created by a font-rendering routine.
- The rendered string is generated by a
sprintf-like function, converting a raw time into the usualmm:ss.tformat. - The raw time is created by a combination of various functions, which compose into this formula:
time_tenths = [DS:12D8] + rand(-64,+63) + (position - 1) * [DS:12DA]
- The pair
[DS:12D8]and[DS:12DA](in green) is track-specific, and comes from an 8-element array (in red) starting at[DS:12DC]:000012d0 02 01 00 02 01 02 02 02 9a 03 0e 00 7a 03 0a 00 000012e0 94 02 08 00 e4 02 0a 00 02 03 0a 00 f8 02 0a 00 000012f0 e8 03 0c 00 2a 03 0a 00 84 03 0c 00 16 13 16 13
Finally the data affecting qualification times were found! For each track, the array at [DS:12DC] defines two 16-bit numbers: one is the expected time of the first AI driver, the other is the average gap between each driver and the following. The physical values of the parameters are these:
| Track | Time | Gap |
|---|---|---|
| Brazil | 1:29.0 | 1.0 |
| Monaco | 1:06.0 | 0.8 |
| Canada | 1:14.0 | 1.0 |
| Detroit | 1:17.0 | 1.0 |
| Britain | 1:16.0 | 1.0 |
| Germany | 1:40.0 | 1.2 |
| Italy | 1:21.0 | 1.0 |
| Japan | 1:30.0 | 1.2 |
Let’s have a second look at the formula: the average span between first and last driver’s time is 9 seconds, but the time of a specific position ranges in an interval of 12.7 seconds, depending on how the RNG rolls. In other words, unless a player is sufficiently fast or slow, luck plays an important role in their starting position.
To validate the analysis, nothing better than some experimental data. Fortunately, the website high-voltage.cz has published a collection of qualification results for Monaco. The data confirm the plausibility of the formula I derived, and also illustrate the influence of the RNG: even in that small dataset, a time of 1:08.0 may be worth pole position or a sixth-place start, depending on your luck.
This concludes the search for the qualification time2, but the investigation got me a small extra reward.
The GPC font
Analyzing the rendering of the qualification result screen, it turned out that GPC does not use printf to display text: even if the typeface appears the same as the default EGA font “IBM BIOS”, the game paints the letters from a set of glyphs it holds in memory. Is the font really identical to the one in the BIOS? To find out, we must look at the glyphs, which are stored as 1-bpp, 8x8 bitmaps, starting from offset 0x79B2. One can convert them quickly with a trick I learned from a Hacker News comment:
dd if=data-segment.bin bs=1 skip=$(( 0x79b2 )) count=$(( 1000 )) | convert -size 8x1000 -depth 1 GRAY:- glyphs.bmp
# Change `convert` to `display` and omit the filename to show the image directly
The output shows that the font follows the standard IBM code page 437, beginning from space (code 32). But after the “⌂” symbol (code 127) there follow custom glyphs:
Has anyone ever seen these characters in the game? I did not – maybe they were meant for other platforms?
In any case, the discovery of the font encoding allowed me to create a glyph for the Spanish letter eñe (n-with-tilde), which I needed for one of the new opponents.
Epilogue
Eine Geschichte, die ein sinnerfülltes Ende hat, ist eine Täuschung.
Akif Pirinçci, Felidae
The building of Don Matrelli’s Legacy took about two months. Most of my previous projects were carried out in the span of one highly productive weekend; this one was more like a marathon: I had to manage my available free time and motivation, which came in ebbs and flows.
If the list of tasks shown in this series seems overwhelming, you can imagine how it looked like for someone who had to figure it out along the way. Some days the backlog would shrink, other days I had to insert new necessary tasks I had overlooked.
And then, one day, I saw the light at the end of the tunnel: all my assets were complete, the Spa track has been polished till the last byte, the launcher was working, no open points came to mind. I even created a custom logo to sign my creation. There remained a mysterious memory corruption bug – maybe a side effect of putting data in every free nook of the data segment – but it could be mitigated by saving regularly. I’d love to fix it sometime in future, but could not dedicate another month to investigate it.
Time to prepare the release then. A proper homage to the 16-bit era of games also requires a manual of good quality, and I was happy to oblige. It was fast and easy to write it: during those weeks spent working at the mod, a lot of fan fiction about my alternative racing universe had come into my mind, and just needed to be written down.
The rest was the usual small work: creating a public repository to publish the mod and selected tools of the trade, making the source code more presentable, ensuring that I had credited all the authors of the source material.
Finally, after a final and a final-final check, I released Don Matrelli’s Legacy on 21 June.
Was it worth it to spend so many hours on a work that will be played by a few dozens of people, maybe some hundred in case my work gets popular on a social media? My answer is affirmative: I wrote this mod firstly for myself, to homage a game that I liked so much in my childhood and to give myself a new challenge after I became too good for the original adversaries. I’ll be happy if many people enjoy this creation, but my goal was reached the moment I found myself striving to lower my lap time on Möbius by one more tenth.
The story of my mod has crossed many others that would also be worth telling: for example the decades-long efforts to decypher DSI’s asset files, starting from Juha-Matti Liukkonen’s early notes in 1996, or the fall from grace of Don Mattrick, from builder of memorable games to big villain of the Xbox brand. But this series has been long enough.
Thanks for carrying with me till the end! I hope my notes and recollections will motivate someone to pick up the torch and continue the story of an old, almost-forgotten racing game. But let’s close with something to be done away from a computer.
Bonus: Test Drive Unplugged, an outdoor game for two kids
The “two kids” may also be a parent and their child, or two people of any age, though I guess a couple of 30-year-olds are not the right demographics.
The two players are the Driver and the Arbiter. The Driver sits at the wheel of a car (switched off, with pulled handbrake). The Arbiter stands outside, near the windshield, holding a deck of sheets representing various possible road features: straight, right turn, a car to overtake, a closed level crossing, whatever.
Periodically, the Arbiter places one of the pictures on the windshield, or otherwise shows it to the Driver, who must react accordingly: steering the wheel, putting on the turn signal, braking etc.
If the Driver fails to “drive” the car properly, at the sole discretion of the arbiter, the latter will put on the dreaded slide showing a cracked windshield: game over! After that, the players swap roles: this is to keep in check the Arbiter’s arbitrariness.
Feel free to invent a variant where the Driver can happily arrive to destination. For me, discussing with the other player after a crash and getting revenge was part of the fun.