Previously, we explored the general architecture of Vice City and took a quick look at the art direction of my GUI redux. As promised, we will now shift our focus to the scripting engine which executes the gameplay logic and the overall simulation of the game, explain its history, why it exists and how it works.

You're reading the Motor Vehicle Theft series

Motor Vehicle Theft is a hobby reimplementation project of Grand Theft Auto: Vice City, progress and description of which is documented in the articles of this series.

Previous article

Enter the virtual machine

Why it’s there...

Like most titles of the late 1990s and the early 2000s, Grand Theft Auto developers utilized a proprietary scripting language to write code targeting the instruction set architecture of a custom virtual machine made to run the modest simulation of their worlds. These scripts could, but were not limited to: initiating missions, triggering cutscenes and animations, shuffling through subtitles, generating ground, sea and air traffic, spawning pedestrians, emergency services and other entities. The modding community got on top of it quickly and translated it into various human readable forms, but they were always on a much lower and coarser level than the original script files.

Much about the original scripting language was unknown until 2011 when references were found to GTA2Script, the predecessor of the language used in games of the 3D universe (III, VC, SA). Extrapolating from there, the scripting language was almost certainly called GTA3Script (not unlike UnrealScript and others). The mystery ended following the release of the GTA III 10th Anniversary Edition when Rockstar North released the original script files for the first time as a gift to the modding community, providing a never before seen glimpse into how Grand Theft Auto III was made. Now we know it draws strong inspiration from BASIC, which makes sense given the time frame in which GTA 2 and GTA2Script were developed.

Vice City data curiosities

There are nine gangs in Vice City, accessed through scripts as Gang0 through Gang8. These include the Haitians, Cubans, rent-a-cops, wannabees, Diaz's goons, bikers, Vercetti's goons, Golfers and the Army. Cops are not defined as a gang in the backend, they're a special faction entity as their behavior is more complicated than the "roaming herd with guns" mechanics of gangs. There are 20 pedestrian types, of which four are player types. Others are civilian male/female, the nine gang related types, cops, paramedics, firemen, criminals and "special" characters (similar to essential characters in Elder Scrolls games). In the entire game, there are 18 interiors. The game actually considers outside to be the 19th interior. Vice City sure felt bigger in 2002.

In general software engineering, the main motivation for developing virtual machines is removing the dependency of user code on the underlying platform. By selecting a virtual machine as the compilation target, all software written for it will work on every platform on which said virtual machine is implemented. I am intentionally using the generic word "platform" as it doesn't necessarily mean shifting between fundamentally different hardware instruction set architectures. You could just move to a different operating system on the same underlying ISA, booting off a different partition on the same machine.

It was not the primary reason why so many game/engine scripting languages and associated virtual machines were devised, though. Developing a simple, high level scripting language along with a domain specific virtual machine allowed less technical personnel to help develop the game. Perhaps the biggest reason for introducing a scripting language is iteration speed. The compile times for large C++ projects (in which game technology is predominantly written) are substantial. Changing, replacing and deleting scripts is fast which means iterating the game is fast. The trade-off here is the that script execution is inherently slower than native code.

Let's jmp into it

...and how it works

If we are to get anywhere, we need to study the instruction set architecture of Vice City’s underlying VM / scripting engine in order to replicate it. To give you a taste, the first three opcodes map to instructions NO_OP, WAIT, and JMP, nothing you haven't seen before. The jump instruction takes a single parameter, a label, which instructs the thread to begin executing instructions where the label is defined. The wait instruction takes a timeout period in milliseconds as a parameter where 0 has special meaning, it instructs the VM to suspend and return to the thread as soon as possible. In essence, it's a yield. A significant number of instructions is dedicated to loading and storing data in global and local variables, as well as performing arithmetic and logical ops on them, to the point there is even an instruction to compare a global to a local float.

As you might expect, there are instructions which enable basic flow control such as 0x004C / 0x004D JMP_IF_#. There are no dedicated loop instructions, so high level GTA3Script loops all get ridiculously unrolled or transformed to conditional label jumps when compiled to bytecode. The vast majority of instructions deal with interfacing with the engine to load models, textures, animations, cutscenes and perform ingame actions which affect the game world, everything from controlling time of day to controlling characters. 0x004E TERMINATE_THREAD and 0x004F CREATE_THREAD help you regulate thread/script execution. It fundamentally comes down to going through each supported instruction and implementing a code path to handle it in the same way the original game does as it goes through the compiled bytecode.

Vice City VM specifications

Like Grand Theft Auto III before it, the extended VM in Vice city runs a maximum of 128 threads, each has 16 local variables with two dedicated timers, maximum subroutine depth is six. There are 1448 instructions of various complexity (used and otherwise) which are coded as monotonically increasing 16-bit opcodes ranging from 0x0000 (NO_OP) to 0x05A8 (GET_OBJECT_SPEED).

By the end of development of the last 3D universe title, Grand Theft Auto: San Andreas, Rockstar's virtual machine had 2639 individual instructions, many of them being unused legacy bloat.

Anatomy of an SCM file

The mother of all coalesced script blobs

The compiled bytecode of all scripts is stored in SCM files and Vice City only has one, an exposed binary main.scm. If you open it up in a hex editor, you will see the first header section is comprised of 02 00 01 20 86 00 00 6D and tens of thousands of zeroed out bytes. This is a reserved space with only a single byte of metadata, the game the SCM file is associated with (6D 'm' stands for Miami). The headers contain crucial information required to navigate and use this file successfully and are always identified with 02 00 01 and immediately followed by a 32-bit offset (first one being 0x00008620) to the second header section.

It contains an array of all used objects and their count (stored as a 32-bit value). The objects themselves are identified by their name which, just like in the image directory index, are allotted 24 bytes (padded if unused), alongside yet another indirection to a third header section which, in turn, contains the offset to the main script, its size, the number of mission scripts and special mission scripts as well as the largest mission script size. The previously mentioned offset is also used as an alignment for all the mission scripts that are present in the SCM, simplifying indexing at the cost of space.

On the SCM abbreviation

"SCM" is one of the remaining mysteries as it was never revealed what M stands for. Obviously, the "SC" stands for script, however the "M" may stand for anything from "Multifile" to "Machine code". In the end, it does not matter. Just a curiosity if you were wondering.

Manually "disassembling" a portion of the main script's bytecode

Following the last two paragraphs, we find that the main script starts at an offset of 0x00009AE4, here's a chunk of it:

A4 03 4D 41 49 4E 00 CC CC CC 6A 01 04 00 04 00
2C 04 04 58 0D 03 05 9A 00 F0 01 04 04 ED 02 04
64 11 01 04 00 C0 00 04 16 04 00 E4 04 06 00 00
A6 42 06 33 73 54 C4 CB 03 06 00 00 A6 42 06 33
73 54 C4 06 CD CC 14 41 53 00 04 00 06 00 00 A6
...

The first opcode is 0x03A4 name_thread, mapping to the instruction specifying the name of the script/thread by means of an 8-byte null-terminated string parameter. 4D 41 49 4E 00 CC CC CC is ASCII-encoded "MAIN \0 ÌÌÌ". The following instruction is 0x016A fade_screen which takes two integers (of what size? We'll get to that in a bit). First specifies the duration of the fade in/out in milliseconds and the second one is a binary switch, 0 is fade out and 1 is fade in. But what is this? 04 00 04 00 suggests we have two unsigned 16-bit integers (4 and 4).

That can't be, right? Correct, the first byte after the opcode signifies the data type in question. In this case, 0x04 means "immediate 8-bit integer" which means the parameter values are actually 0 and 0, instructing the game to fade out immediately. The data type assists you when going through the bytecode to properly read in the parameter size so you can move along to the next opcode instead of having a map between opcodes and types of each parameter. Unfortunately, Rockstar was not known for consistency back in the day so some instructions do not come with data type definitions, such as the 0x03A4 name_thread.

Writing it out

By reverse engineering the opcodes (done by the modding community over a decade ago) and using them as reference, you can get an assembly-like output using mnemonics to get an idea of what the original GTA3Script was written to compute:

/*

A4 03 4D 41 49 4E 00 CC CC CC 6A 01 04 00 04 00
2C 04 04 58 0D 03 05 9A 00 F0 01 04 04 ED 02 04
64 11 01 04 00 C0 00 04 16 04 00 ...

*/

A4 03    ?? 4D 41 49 4E 00 CC CC CC 0x03A4 'MAIN \0 ÌÌÌ'    0x03A4 name_thread 'MAIN' // Data type should be 09, immediate null terminated 8-byte string
6A 01    04 00    04 00             0x016A 0x0 0x0          0x016A fade 0 0
2C 04    04 58                      0x042C 0x58             0x042C set_missions_count 88
0D 03    05 9A 00                   0x030D 0x009A           0x030D set_mission_points 154
F0 01    04 04                      0x01F0 0x4              0x01F0 set_max_wanted_lvl 4
ED 02    04 64                      0x02ED 0x64             0x02ED set_hidden_pkg_count 100
11 01    04 00                      0x0111 0x0              0x0001 set_wb_check 0
C0 00    04 16    04 00             0x00C0 0x16 0x0         0x00C0 set_time 22 0
// And so on and so forth...

You might be thinking that this is only needed for those who want to modify the game by writing or editing scripts, but understanding the broad strokes of the VM and what the scripts are trying to do is crucial to debugging your VM replica that will be communicating with the rest of your engine written in C++ to actuate things on screen. And all you have to do is go through this list and implement everything used in Vice City's scripts. In the next article, we'll be discussing how I've been doing just that!

Be careful what you wish for

While this may seem like a chore, to implement hundreds of instructions of an ancient VM ISA, consider that the scripts call on these building blocks hundreds of thousands of times in order to define the gameplay and simulation of Vice City. So, which would you rather reimplement? I thought so.

The mission construct

Addendum per request: Clarifying the spork of the VC VM.

The mission is at the core of all gameplay in Vice City. However, it is used for more than just implementing missions. At the start of each game, the 0th mission (INITIAL) sets up the state of the game, even prepares a bunch of parked cars generators which instruct the game to spawn cars when the player is close enough. Any kind of special cutscene sequence is also made a mission regardless of not having any gameplay (beyond skipping it), an example being the INTRO mission.

The reason missions are used over merely starting simple threads is because the special instructions for loading and terminating missions do implicit state changes adjusting the way the game works, moving it from the chaotic open world mode to the controlled linear mode for which Rockstar is (in)famous for, depending on who you ask. Behind the scenes, starting a mission is more or less starting a thread locked onto a mission script with special state changes which would be error prone to do manually.