SCM Instruction
Each script instruction is represented by a number called an opcode (short for "operation code") which is a 16 bit unsigned integer. The opcode tells the game which operation to perform. For example, the opcode 0001 tells the game to wait for a certain amount of time, 0003 shakes the game camera and 0053 creates a player.
Contents
Instruction Format
Initially the instructions are written as text. For example, a wait
instruction could look like this:
wait 0
where wait
represents the opcode 0001
and the number 0
is an argument passed with the instruction. Different compilers may use different words to represent each opcode. When the code is compiled, the instruction is converted to raw bytes:
01 00 04 00
- The first two bytes (
01 00
hex) are the opcode bytes in little-endian order. - The third byte (
04
hex) is the code for the data type.[*]0x4
is the code for a signed byte. - The final byte (
00
hex) is the argument value. This is often more than one byte: the size is determined by the data type.
While the opcode bytes are always present, there may not always be arguments passed, so there may be no argument bytes.
Although an unsigned 16-bit integer (uint16
) can hold values in the range 0x0000
- 0xFFFF
, the range of used opcodes in unmodded games is much smaller. For example, San Andreas has opcodes going up to 0x0A4E
. There are tools that add more instructions to the game, most notably the CLEO Library.
Any values over the signed 16-bit integer maximum value (0x7FFF
) denote negative conditional opcodes, which suggests that the opcodes are not always treated as unsigned integers by the game.
Arguments
Each instruction takes a certain number of arguments. If a script passes an incorrect number of arguments, the game will crash.
An argument could be one of the following types:
- Immediate values
- Integer (
1
,324
) - Float (
0.43
,672.0
) - Strings (
"abc"
,""
)
- Integer (
- Variables
- Global variables
- Local variables
- Arrays
Concrete data types
Each of the types listed above can be represented in a number of ways in compiled code for various reasons:
- Integer values can be signed or unsigned, and there are also different sizes of integer that the game can use.
- There are multiple types of string that allow different numbers of characters.
- Variables use different type codes based on the type of value that they are referencing.
The concrete type of an argument is determined by a single byte before the value bytes[*]. This byte tells the game what value is coming next so that it knows how many bytes to read, and how to treat the value once read.
Type code (hex) |
Value length (bytes) |
Support | Description | ||
---|---|---|---|---|---|
00 | 0 | End of argument list (EOAL, 004F or 0913 and similar)[*] | |||
01 | 4 | Immediate 32-bit signed int
scriptParam.m_iIntValue = *(int *)m_pScriptPC;
m_pScriptPC += 4; | |||
02 | 2 | Global integer/floating-point variable
scriptParam.m_usGlobalOffset = *(unsigned short *)m_pScriptPC;
m_pScriptPC += 2; | |||
03 | 2 | Local integer/floating-point variable
scriptParam.m_sLocalVar = *(short *)m_pScriptPC;
m_pScriptPC += 2; | |||
04 | 1 | Immediate 8-bit signed int
scriptParam.m_iIntValue = *(char *)m_pScriptPC++; | |||
05 | 2 | Immediate 16-bit signed int
scriptParam.m_iIntValue = *(short *)m_pScriptPC;
m_pScriptPC += 2; | |||
06 | 2 | Immediate 16-bit fixed-point (see remark)
scriptParam.m_fFloatValue = (float)(*(short *)m_pScriptPC) / 16.0f;
m_pScriptPC += 2; | |||
06 | 4 | Immediate 32-bit floating-point
scriptParam.m_fFloatValue = *(float *)m_pScriptPC;
m_pScriptPC += 4; | |||
07 | 6 | Global integer/floating-point array[*]
scriptParam.m_usGlobalOffset = *(unsigned short *)m_pScriptPC;
scriptParam.m_sArrayIndexVar = *(short *)(m_pScriptPC + 2);
scriptParam.m_ucArraySize = *(unsigned char *)(m_pScriptPC + 4);
scriptParam.m_arrayProperties = *(ArrayProperties *)(m_pScriptPC + 5);
m_pScriptPC += 6; | |||
08 | 6 | Local integer/floating-point array[*]
scriptParam.m_sLocalVar = *(short *)m_pScriptPC;
scriptParam.m_sArrayIndexVar = *(short *)(m_pScriptPC + 2);
scriptParam.m_ucArraySize = *(unsigned char *)(m_pScriptPC + 4);
scriptParam.m_arrayProperties = *(ArrayProperties *)(m_pScriptPC + 5);
m_pScriptPC += 6; | |||
09 | 8 | Immediate 8-byte string[*]
strcpy(scriptParam.m_szTextLabel, (char *)m_pScriptPC);
m_pScriptPC += 8; | |||
0A | 2 | Global 8-byte string variable
scriptParam.m_usGlobalOffset = *(unsigned short *)m_pScriptPC;
m_pScriptPC += 2; | |||
0B | 2 | Local 8-byte string variable
scriptParam.m_sLocalVar = *(short *)m_pScriptPC;
m_pScriptPC += 2; | |||
0C | 6 | Global 8-byte string array[*]
scriptParam.m_usGlobalOffset = *(unsigned short *)m_pScriptPC;
scriptParam.m_sArrayIndexVar = *(short *)(m_pScriptPC + 2);
scriptParam.m_ucArraySize = *(unsigned char *)(m_pScriptPC + 4);
scriptParam.m_arrayProperties = *(ArrayProperties *)(m_pScriptPC + 5);
m_pScriptPC += 6; | |||
0D | 6 | Local 8-byte string array[*]
scriptParam.m_sLocalVar = *(short *)m_pScriptPC;
scriptParam.m_sArrayIndexVar = *(short *)(m_pScriptPC + 2);
scriptParam.m_ucArraySize = *(unsigned char *)(m_pScriptPC + 4);
scriptParam.m_arrayProperties = *(ArrayProperties *)(m_pScriptPC + 5);
m_pScriptPC += 6; | |||
0E | 1 + (n - 1) | Immediate variable-length string[*] (non null-terminated)
char cStrLength = *(char *)m_pScriptPC++;
strncpy(scriptParam.m_szTextLabel, (char *)m_pScriptPC, cStrLength);
memset(&scriptParam.m_szTextLabel[cStrLength], '\0', ucMaxLength - cStrLength);
m_pScriptPC += cStrLength; | |||
0F | 16 | Immediate 16-byte string[*]
strcpy(scriptParam.m_szTextLabel, (char *)m_pScriptPC);
m_pScriptPC += 16; | |||
10 | 2 | Global 16-byte string variable
scriptParam.m_usGlobalOffset = *(unsigned short *)m_pScriptPC;
m_pScriptPC += 2; | |||
11 | 2 | Local 16-byte string variable
scriptParam.m_sLocalVar = *(short *)m_pScriptPC;
m_pScriptPC += 2; | |||
12 | 6 | Global 16-byte string array[*]
scriptParam.m_usGlobalOffset = *(unsigned short *)m_pScriptPC;
scriptParam.m_sArrayIndexVar = *(short *)(m_pScriptPC + 2);
scriptParam.m_ucArraySize = *(unsigned char *)(m_pScriptPC + 4);
scriptParam.m_arrayProperties = *(ArrayProperties *)(m_pScriptPC + 5);
m_pScriptPC += 6; | |||
13 | 6 | Local 16-byte string array[*]
scriptParam.m_sLocalVar = *(short *)m_pScriptPC;
scriptParam.m_sArrayIndexVar = *(short *)(m_pScriptPC + 2);
scriptParam.m_ucArraySize = *(unsigned char *)(m_pScriptPC + 4);
scriptParam.m_arrayProperties = *(ArrayProperties *)(m_pScriptPC + 5);
m_pScriptPC += 6; | |||
No type code | |||||
N/A | 8 | Immediate 8-byte string[*]
strcpy(scriptParam.m_szTextLabel, (char *)m_pScriptPC);
m_pScriptPC += 8; | |||
N/A | 128 | Immediate 128-byte string
strcpy(scriptParam.m_szString, (char *)m_pScriptPC);
m_pScriptPC += 128; |
Type codes for Liberty City Stories and Vice City Stories are very different from other games:
- In some instances, the type code itself denotes the argument value. For example:
0x1
represents the integer value 00x2
represents 0.0
- The type code can sometimes denote a variable.
- Floating point values are packed (8, 16 or 24 bits as opposed to the more common 32 bits).
Type code (hex) |
Value length (bytes) |
Support | Description | |
---|---|---|---|---|
Typified | ||||
00 | 0 | End of argument list (EOAL) | ||
01 | 0 | Immediate 8-bit signed integer constant 0
scriptParam.m_iIntValue = 0; | ||
02 | 0 | Immediate 8-bit floating-point constant 0.0
scriptParam.m_fFloatValue = 0.0f; | ||
03 | 1 | Immediate 8-bit packed floating-point
unsigned int uiUnpackedFloat = *(unsigned char *)m_pScriptPC++ << 24;
scriptParam.m_fFloatValue = *(float *)&uiUnpackedFloat; | ||
04 | 2 | Immediate 16-bit packed floating-point
unsigned int uiUnpackedFloat = *(unsigned short *)m_pScriptPC << 16;
scriptParam.m_fFloatValue = *(float *)&uiUnpackedFloat;
m_pScriptPC += 2; | ||
05 | 3 | Immediate 24-bit packed floating-point
unsigned int uiUnpackedFloat
= (*(unsigned short *)m_pScriptPC << 16)
| (*(unsigned char *)(m_pScriptPC + 2) << 8);
scriptParam.m_fFloatValue = *(float *)&uiUnpackedFloat;
m_pScriptPC += 3; | ||
06 | 4 | Immediate 32-bit signed integer
scriptParam.m_iIntValue = *(int *)m_pScriptPC;
m_pScriptPC += 4; | ||
07 | 1 | Immediate 8-bit signed integer
scriptParam.m_iIntValue = *(char *)m_pScriptPC++; | ||
08 | 2 | Immediate 16-bit signed integer
scriptParam.m_iIntValue = *(short *)m_pScriptPC;
m_pScriptPC += 2; | ||
09 | 4 | Immediate 32-bit floating-point
scriptParam.m_fFloatValue = *(float *)m_pScriptPC;
m_pScriptPC += 4; | ||
0A | n + NUL | Immediate null-terminated string[*]
strcpy(scriptParam.m_szTextLabel, (char *)m_pScriptPC);
m_pScriptPC += strlen((char *)m_pScriptPC) + 1; | ||
Untypified (script variables) | ||||
0A..0B | 1 | Local timers (TIMERA, TIMERB)
scriptParam.m_sLocalVar = *(unsigned char *)m_pScriptPC++ + 0x5E; | ||
0B..0C | 1 | Local timers (TIMERA, TIMERB)
scriptParam.m_sLocalVar = *(unsigned char *)m_pScriptPC++ + 0x5D; | ||
0C..6B | 1 | Local integer/floating-point variable
scriptParam.m_sLocalVar = *(unsigned char *)m_pScriptPC++ - 0x0C; | ||
0D..6C | 1 | Local integer/floating-point variable
scriptParam.m_sLocalVar = *(unsigned char *)m_pScriptPC++ - 0x0D; | ||
6C..CB | 3 | Local integer/floating-point array
scriptParam.m_sLocalVar = *(unsigned char *)m_pScriptPC - 0x6C;
scriptParam.m_sArrayIndex = *(unsigned char *)(m_pScriptPC + 2);
scriptParam.m_ucArraySize = *(unsigned char *)(m_pScriptPC + 3);
m_pScriptPC += 3; | ||
6D..CC | 3 | Local integer/floating-point array
scriptParam.m_sLocalVar = *(unsigned char *)m_pScriptPC - 0x6D;
scriptParam.m_sArrayIndex = *(unsigned char *)(m_pScriptPC + 2);
scriptParam.m_ucArraySize = *(unsigned char *)(m_pScriptPC + 3);
m_pScriptPC += 3; | ||
CC..E5 | 2 | Global integer/floating-point variable[*]
unsigned short usBigEndianWord = *(unsigned short *)m_pScriptPC - 0x00CC;
scriptParam.m_sGlobalVar = (short)((usBigEndianWord << 8) | (usBigEndianWord >> 8));
m_pScriptPC += 2; | ||
CD..E5 | 2 | Global integer/floating-point variable[*]
unsigned short usBigEndianWord = *(unsigned short *)m_pScriptPC - 0x00CD;
scriptParam.m_sGlobalVar = (short)((usBigEndianWord << 8) | (usBigEndianWord >> 8));
m_pScriptPC += 2; | ||
E6..FF | 4 | Global integer/floating-point array
unsigned short usBigEndianWord = *(unsigned short *)m_pScriptPC - 0x00E6;
scriptParam.m_sGlobalVar = (short)((usBigEndianWord << 8) | (usBigEndianWord >> 8));
scriptParam.m_sArrayIndex = *(unsigned char *)(m_pScriptPC + 2);
scriptParam.m_ucArraySize = *(unsigned char *)(m_pScriptPC + 3);
m_pScriptPC += 4; | ||
N/A | 8 | Immediate 8-byte string
strcpy(scriptParam.m_szTextLabel, (char *)m_pScriptPC);
m_pScriptPC += 8; |
^ This type was introduced in VCS due to the presence of string variables.
^ Given the data type range limit the largest global variable in LCS is $6655
, in VCS $6399
.
All the data types above haven't been tested in a decompiling process yet, hence they still need a practical confirmation.
This section is incomplete. You can help by fixing and expanding it.
Integer numbers
An integer is a number without a decimal or fractional component.
Size (bytes) |
Range | |||||
---|---|---|---|---|---|---|
Signed | Name | Unsigned | Name | |||
1 | -128 to 127 | INT8, CHAR | 0 to 255 | UINT8, BYTE | ||
2 | -32,768 to 32,767 | INT16, SHORT | 0 to 65,535 | UINT16, WORD, USHORT | ||
4 | -2,147,483,648 to 2,147,483,647 | INT32, LONG | 0 to 4,294,967,295 | UINT32, DWORD, ULONG |
Floating-point numbers
A floating point number is a number with a decimal component.
Size (bytes) |
Range | Name |
---|---|---|
4 | ±1.1754944×10-38 to ±3.4028234×1038 | SINGLE, FLOAT |
Strings
A string is a piece of text. Strings can include letters, numbers and symbols.
There are two kinds of string:
- Fixed-length. This is the most common type of string and has been used since GTA 3. The string length is fixed. When compiled these strings occupy a certain number of bytes (8 or 16) even if the text is actually shorter (any unused bytes are filled with null values).
- Variable length (SA only). Variable-length strings are encoded as a single byte specifying the length followed by the string character bytes. These strings are not null-terminated. The maximum length depends on the instruction[*]. The longest in the original game is 40 characters.
String value | Compiled bytes |
---|---|
"MAIN" |
09 4D 41 49 4E 00 00 00 00 |
"MODDING" |
09 4D 4F 44 44 49 4E 47 00 |
"SAVE_OUR_SOULS!" |
0F 53 41 56 45 5F 4F 55 52 5F 53 4F 55 4C 53 21 00 |
"Variable length string" |
0E 16 56 61 72 69 61 62 6C 65 20 6C 65 6E 67 74 68 20 73 74 72 69 6E 67 |
Arrays
In GTA SA, an array is a static reference to a group of successive variables. There is no actual representation of an array value as a concrete type, they do have type codes. When an "array" is passed as an argument to an instruction, what is actually passed is a specific variable in that array.
This section describes the format of array accesses in GTA SA. Vice City also has arrays, but in a different format.
// Example: not real code.
// Applies only to GTA: SA.
struct ArrayAccess {
enum ElementType : uint8_t {
Int,
Float,
String8,
String16
};
// Offset of first variable in the array.
uint16_t startOffset;
// Index being accessed ("array[index]").
// This can be a local variable ("array[someLocalVar]") or a global variable ("array[someGlobalVar]").
uint16_t index;
// Array length.
int8_t length;
// Array element type. 7 bits.
ElementType type : 7;
// Determines whether the index is a global variable (true) or a local variable (false).
// Only 1 bit.
bool globalIndex : 1;
};
The array offset is a variable which should have a value more than or equal to 0 and smaller than the array size. Global and local variables can be used as index variables – the purpose of the globalIndex
field is to tell the game which has been used. The first element in the array is at index 0
, and the last is at length - 1
.
Array | Equivalent in SCM |
---|---|
$150(3@,6f) | 07 96 00 03 00 06 01 |
10@(9@,5s) | 0D 0A 00 09 00 05 02 |
Notes
^ In GTA 3, Vice City and Liberty City Stories, short strings (8 characters) have no type code. If the first byte of an argument does not fit data type range (0x0
- 0x6
for GTA 3 and VC), it's recognized as the beginning of a string and the remaining 7 bytes of the string are read.
^ Some instructions have a variable number of parameters. One such instruction is 004F that creates a new thread. To allow extra script setup, the instruction can take a variable number of arguments. The game uses the special data type to end such argument lists.
For non-variadic instructions, the maximum number of parameters is 16 for GTA 3 and VC and 32 for SA, LCS and VCS. Variadic instructions can be passed a maximum of 18 arguments in GTA 3 and VC; 34 for SA; and 106 for LCS and VCS, though these values are unconfirmed.
^ In , 05B6 is a special instruction that defines a table. Immediately after the opcode a 128 byte stream of data follows, without a type code.
^ GTAForums: Post by Seemann describing limits for the long strings in SA