Difference between revisions of "SCO"

From GTAMods Wiki
Jump to navigation Jump to search
(Opcodes: Updated protect opcodes, removed Abort opcode (its not really an opcode, but rather an error condition))
Line 29: Line 29:
 
====Opcodes====
 
====Opcodes====
  
Opcodes can have varying sizes, but all opcodes are identified by their first byte. There are 80 opcodes which can occur, any opcode above 80 is a Push opcode which pushes '''it's own number - 96''' onto the stack.
+
Opcodes can have varying sizes, but all opcodes are identified by their first byte. There are 79 opcodes which can occur and any opcode above 80 is a Push opcode which pushes '''it's own number - 96''' onto the stack. For example, opcode 100 will push 4 onto the stack. Opcodes 76,77,78 deal with XLive protected buffers and are available on the PC platform only. Undefined opcodes (i.e. opcode 79), will cause a forceful abort of the script execution.
  
 
{|{{Prettytable}} class="collapsible"
 
{|{{Prettytable}} class="collapsible"
Line 175: Line 175:
 
|75|| StrVarCpy || Pops 3 items off the stack. Copies the third item's memory into the first item's memory with repeating defined by the second item. It then appends a null terminator to the first item's memory || 1 byte
 
|75|| StrVarCpy || Pops 3 items off the stack. Copies the third item's memory into the first item's memory with repeating defined by the second item. It then appends a null terminator to the first item's memory || 1 byte
 
|-
 
|-
|76|| GetProtect || Pops a memory location off the stack and pushes its protection details onto the stack || 1 byte
+
|76|| GetProtect || Pops a memory location off the stack and pushes its XLive unprotected value onto the stack (PC Only) || 1 byte
 
|-
 
|-
|77|| SetProtect || Pops a memory location off the stack as well as its protection details and sets the memory locations new protect details || 1 byte
+
|77|| SetProtect || Pops a memory location off the stack, pops another value off the stack. Protects the second value using XLive and stores the value in the memory location. (PC Only) || 1 byte
 
|-
 
|-
|78|| RefProtect || ? || 1 byte
+
|78|| RefProtect || Pops a memory location off the stack, pops another value off the stack to determine protection flags, and finally pops another value which indicates the number of items to operate on. If the 1st bit of the flags is set, the number of items at the memory location will be XLive unprotected. If the 2nd bit is set, they will be XLive protected instead. (PC Only) || 1 byte
|-
 
|79|| Abort || Aborts the script || 1 byte
 
 
|}
 
|}
  

Revision as of 17:15, 24 February 2009

SCO files contain GTA 4's game scripts. Its new format replaces old scm one.

File Format

A SCO file is layed out into 4 segments. First the header containing information about the SCO file. Then the code segment which contains the opcode's which govern how the script behaves. The next segment is the local variables container which contains enough space to hold the local variables. The last is the global variables container, which contains enough space to house the global variables, as well as setting them by default.

Header

There are 2 types of SCO files, an encrypted and unencrypted one. Each file however shares the same unencrypted header structure, and you can use this to determine which type of SCO file you are dealing with. The size of this header is 24 bytes.

4b - CHAR[4]/UINT32 - SCO Identifier
4b - UINT32 - Code Size
4b - UINT32 - Local Var Size
4b - UINT32 - Global Var Size
4b - UINT32 - Script Flags
4b - UINT32 - Signature

The SCO Identifier will be "SCR\r" (or 0xD524353) in an unencrypted version, and "scr"+0xE (or 0xE726373) in an encrypted version. To decrypt an encrypted version you must decrypt each segment (except the header) separately using GTA IV's AES Cryptography. The Code Size refers to the amount of bytes the code section takes up. The Local Var Size refers to the amount of local variables the SCO file contains. The segment for local variables starts at the end of Header + Code Size, and continues for 4x the local variable size (due to the local variables being stored in 4 byte segments). The Global Var Size refers to the amount of global variables the SCO file contains. The segment for global variables starts at the end of Header + Code Size + Local Var Size * 4, and continues for 4x the global variable size (due to the global variables being stored in 4 byte segments). The Script Flags are boolean bits which are currently unexplained. The Signature only differs in navgen_main, but could possibly set the script priority.

Code Segment

The Code Segment contains the opcodes which govern the scripts behaviour.

Opcodes

Opcodes can have varying sizes, but all opcodes are identified by their first byte. There are 79 opcodes which can occur and any opcode above 80 is a Push opcode which pushes it's own number - 96 onto the stack. For example, opcode 100 will push 4 onto the stack. Opcodes 76,77,78 deal with XLive protected buffers and are available on the PC platform only. Undefined opcodes (i.e. opcode 79), will cause a forceful abort of the script execution.

ID Name Description Length
0 Nop No operation 1 byte
1 Add Adds the top 2 items on the stack 1 byte
2 Sub Subtracts the top 2 items on the stack 1 byte
3 Mul Multiplies the top 2 items on the stack 1 byte
4 Div Divides the top 2 items on the stack 1 byte
5 Mod Mods the top 2 items on the stack 1 byte
6 IsZero Checks the first item on the stack to see if it equals 0 1 byte
7 Neg Reverses the sign on the item on the top of the stack 1 byte
8 CmpEq Compares the top 2 integers on the stack to see if they are equal 1 byte
9 CmpNe Compares the top 2 integers on the stack to see if they are not equal 1 byte
10 CmpGt Compares the top 2 integers on the stack to see if the first one is greater than the second one 1 byte
11 CmpGe Compares the top 2 integers on the stack to see if the first one is greater than or equal to the second one 1 byte
12 CmpLt Compares the top 2 integers on the stack to see if the first one is less than the second one 1 byte
13 CmpLe Compares the top 2 integers on the stack to see if the first one is less than or equal to the second one 1 byte
14 AddF Adds the top 2 floats on the stack 1 byte
15 SubF Subtracts the top 2 floats on the stack 1 byte
16 MulF Multiplies the top 2 floats on the stack 1 byte
17 DivF Divides the top 2 floats on the stack 1 byte
18 ModF Mods the top 2 floats on the stack 1 byte
19 NegF Reverses the sign on the first float on the stack 1 byte
20 CmpEqF Compares the top 2 floats on the stack to see if they are equal 1 byte
21 CmpNeF Compares the top 2 floats on the stack to see if they are not equal 1 byte
22 CmpGtF Compares the top 2 floats on the stack to see if the first one is greater than the second one 1 byte
23 CmpGeF Compares the top 2 floats on the stack to see if the first one is greater than or equal to the second one 1 byte
24 CmpLtF Compares the top 2 floats on the stack to see if the first one is less than the second one 1 byte
25 CmpLeF Compares the top 2 floats on the stack to see if the first one is less than or equal to the second one 1 byte
26 AddVec Adds the top 2 Vectors[1] on the stack 1 byte
27 SubVec Subtracts the top 2 Vectors[1] on the stack 1 byte
28 MulVec Multiplies the top 2 Vectors[1] on the stack 1 byte
29 DivVec Divides the top 2 Vectors[1] on the stack 1 byte
30 NegVec Reverses the sign on the first vector[1] on the stack 1 byte
31 And Performs an And operation to the first 2 integers on the stack 1 byte
32 Or Performs an Or operation to the first 2 integers on the stack 1 byte
33 Xor Performs a Xor operation to the first 2 integers on the stack 1 byte
34 Jump Jumps to a section of code, using the next 4 bytes after the opcode as a placement 5 bytes
35 JumpFalse Jumps to a section of code if the top of the stack is 0, using the next 4 bytes after the opcode as a placement 5 bytes
36 JumpTrue Jumps to a section of code if the top of the stack is 1, using the next 4 bytes after the opcode as a placement 5 bytes
37 ToF Converts the top integer on the stack to a float, and puts that float on the stack 1 byte
38 FromF Converts the top float on the stack to an integer, and puts that integer on the stack 1 byte
39 VecFromF Converts the top float into a Vector[1] containing 3 instances of the same float, and pushes the pointer to that Vector[1] onto the top of the stack 1 byte
40 PushS Pushes a short onto the stack, the short is defined in the next 2 bytes after the opcode 3 bytes
41 Push Pushes an int onto the stack, the integer is defined in the next 4 bytes after the opcode 5 bytes
42 PushF Pushes a float onto the stack, the float is defined in the next 4 bytes after the opcode. Performs exactly the same as Push 5 bytes
43 Dup Duplicates the first item on the stack, and pushes it back onto the stack 1 byte
44 Pop Pops the top item off the stack 1 byte
45 CallNative Calls a native function. The number of arguments for the native to take is defined in the next byte after the opcode. The number of return values is defined in the byte after that (it is always 1 or 0). The next 4 bytes are the hash of the native's name. 7 bytes
46 Call Calls a function within the script, and puts the return address on top of the stack. The location of the function is defined in the next 4 bytes after the opcode 5 bytes
47 FnBegin Indicates the beginning of an internal function. The byte after the opcode indicates the amount of arguments the function takes off the stack, and the next 2 bytes after that indicate the number of variables the function will have to generate on the stack. 4 bytes
48 FnEnd Indicates the end of an internal function. The byte after the opcode indicates the amount of arguments that will have to be popped off the stack, and the next byte after that indicates the stack number of the return address 3 bytes
49 RefGet Pops a pointer off the stack and pushes the value stored in that pointer back onto the stack 1 byte
50 RefSet Pops 2 items off the stack and stores the second item at the location of the first item (the first item being a pointer) 1 byte
51 RefPeekSet Pops the first item off the stack and peeks at the second item on the stack, then stores the first item at the location pointed to by the second item on the stack 1 byte
52 ArrayExplode Pops 2 items off the stack, the first and second items being the beginning of the memory of the array and the end of the memory of the array. It then divides the difference of these 2 locations by 4 to get the number of items in the array, after which is pushes these items one by one onto the stack in 4 byte segments 1 byte
53 ArrayImplode Pops the first item off the stack to get the address of the array to write to, and then pops the stack off onto that array 1 byte
54 -> 61 LocalVarPtr Pushes the pointer to a local variable onto the stack 1 byte
62 LocalVarPtrEx Pushes the pointer to a local variable onto the stack where the index is above or equal to 8 1 byte
63 LocalVar Pops the index of the local variable off the stack, and pushes a pointer to a script local variable onto the stack 1 byte
64 GlobalVar Pops the index of the global variable off the stack, and pushes a pointer to a script global variable onto the stack 1 byte
65 ArrayRef Pops the array location, element size and index off the stack, the pushes a pointer to the index of that array onto the stack 1 byte
66 Switch Pops the item to compare off the stack, and then jumps to location corresponding to that item. After the opcode byte it contains a byte defining the number of possible entries, and after that the number of possible entries times 8 are taken up with repeating instances of 4 bytes of the index identifier, and 4 bytes of the location to jump to if that index is correct (Byte after opcode * 8) + 2
67 PushString Pushes a string onto the stack. The byte after the opcode signals the string length, and for the amount of string length after that byte contains each character of the string (Byte after opcode)+2
68 NullObj Pushes a pointer to an empty memory container onto the stack 1 byte
69 StrCpy Pops 2 pointers off the stack, and copies the second item to the first item 1 byte
70 IntToStr Pops an integer off the stack and pushes an array to a string representation of that integer onto the stack 1 byte
71 StrCat Pops 2 pointers off the stack, and appends the second item to the first item 1 byte
72 StrCatI Pops 2 items off the stack, and performs a IntToStr on the second item (the integer), then appends that string representation to the first item 1 byte
73 Catch Sets up a safe area that has the ability to catch errors 1 byte
74 Throw Indicates an area that handles a script error relative to the catch opcode 1 byte
75 StrVarCpy Pops 3 items off the stack. Copies the third item's memory into the first item's memory with repeating defined by the second item. It then appends a null terminator to the first item's memory 1 byte
76 GetProtect Pops a memory location off the stack and pushes its XLive unprotected value onto the stack (PC Only) 1 byte
77 SetProtect Pops a memory location off the stack, pops another value off the stack. Protects the second value using XLive and stores the value in the memory location. (PC Only) 1 byte
78 RefProtect Pops a memory location off the stack, pops another value off the stack to determine protection flags, and finally pops another value which indicates the number of items to operate on. If the 1st bit of the flags is set, the number of items at the memory location will be XLive unprotected. If the 2nd bit is set, they will be XLive protected instead. (PC Only) 1 byte

^ The Vectors on the stack are pointers to the memory containing the full vector. A Vector is characterised by this structure:

4b - FLOAT32 - X
4b - FLOAT32 - Y
4b - FLOAT32 - Z

Local Variables

This contains the Local Variables in the script. Each local variable is 4 bytes long, and can contain static information in the script file itself.

Global Variables

This contains the Global Variables in the script. Each global variable is 4 bytes long, and can contain static information in the script file itself. A global variable remains static until changed via an opcode (or memory editing).

High Level Representation

To turn the assembly of a SCO file into a high level representation some factors have to be considered. Arrays and structures can be defined, so it is reasonable to assume there is some kind of typecasting, even if it is only between the following types; int, float, string or a predefined structure. It is interesting to note that StrCpy, StrCat, StrCatInt and IntToStr all exist as opcodes, this makes it feasible to believe that strings may have a Java like syntax when being manipulated, for example to make the string "Hello World 2009" you could perform this kind of operation:

string testStr = "Hello " + "World " + 2009;

This would bring it on par with many other scripting languages such as LUA. Also like other scripting languages, it's stack just recognises 32 bit numbers (or 4 bytes per stack frame), this means there can be no simple arithmetic done on an opcode level on longs or doubles, so the f character to represent a float as opposed to a double is redundant. When decompiling the script (as in compiling) you can look for certain patterns in the assembly to ascertain what it would like represented at a higher level.

main Function

[0x0] : Jump 0x5

The first opcode in a SCO file is always a jump to the main() function, where the script starts executing. To get the main prototype you can simply check its FnBegin parameters, for example if you jump to code that has FnBegin 1 1, the main function will begin like this:

void main(var arg1)
{
 var fnlc_1;
}

As you can see 1 argument has been added, and 1 local variable has been added. A main function will never include a return value, which makes analysing the main() prototype much easier.

Subroutines

A subroutine can be easily obtained by scanning for FnBegin and FnEnd opcodes. However to see whether it has a return value you must analyse the stack as you make your way through the code. Like the main function before, you can obtain the prototype and the number of function local variables by looking at the FnBegin opcode.

Calling Subroutines

To call a subroutine you will first need it's address. The Call opcode will jump the code to the location defined, and all the parameters will be pushed onto the stack before the call is made.

[0x0] Jump 0x5
[0x5] FnBegin 0 0
[0x9] Call 0x11
[0xE] FnEnd 0 0
[0x11] FnBegin 0 0
[0x15] Push 100
[0x1A] FnEnd 0 1
int sub_11()
{
 return 100;
}

void main()
{
 sub_11();
}

Accessing Parameters

The FnBegin opcode tells us how many parameters the script is expecting, and how many function local variables are created on the stack. To parse parameters to functions the variables must first be pushed to the stack before they are used.

[0x0] Jump 0x5
[0x5] FnBegin 0 0
[0x9] Push 100
[0xE] PushF 100.0
[0x13] Call 0x1B
[0x18] FnEnd 0 0
[0x1B] FnBegin 2 0
[0x1F] FnEnd 2 0
void sub_1B(int arg1,float arg2)
{
}

void main()
{
 sub_1B(100,100.0);
}

Returning Values

If you get to FnEnd and find there is still a value on the top of the stack (inserted by the function after the return address) you will know whether or not there is a return value associated with the function. Whether it uses Push or PushF will determine whether it returns a float or an int. Here is an example of a subroutine returning the float 1.0 in assembly, and it's high level equivalency:

[0x10] FnBegin 0 0
[0x14] PushF 1.0
[0x19] FnEnd 0 1

Notice that a float of 1.0 is pushed before the FnEnd opcode is called. The FnEnd opcode is also called citing a return address at a stack frame of 1, this correct when you consider that 1.0 exists in the stack frame 0.

float sub_10()
{
 return 1.0;
}

Something that those of you who are familiar with ASM will notice is there is no retn Opcode, however you can still support returns in high level code by pushing your return value and jumping to a location at the end of the subroutine.

Accessing Global and Local Variables

To access global and local variables, the opcodes GlobalVar and LocalVar are used. These opcodes simply push the memory address of the variables onto the stack. They can then be manipulated using the RefGet or RefSet opcodes. Here is an example of low level code that will set the global variable 1 to 100, and the local variable 1 to 100.0:

[0x10] Push 100
[0x11] Push 1
[0x12] GlobalVar
[0x13] RefSet
[0x14] PushF 100.0
[0x19] Push 1
[0x1A] LocalVar
[0x1B] RefSet

As you can see local and global variables can be manipulated very easily using the stack. A high level representation of this code would be:

global int gb_1;
local float lc_1;

gb_1 = 100;
lc_1 = 100.0;

Accessing Function Local Variables

To access function local variables (variables that only exist inside the scope of a single function) you must use the opcode LocalVarPtr. This works much the same way as the global and local variables, you push the index of the variable into the opcode and use RefSet. Here is some assembly that sets the function local fclc_1 to 100 and some high level equivalency:

[0x10] FnBegin 0 1 ; 1 variable is defined
[0x14] Push 100
[0x15] Push 0
[0x16] LocalVarPtr
[0x17] RefSet
[0x18] FnEnd 0 0
void sub_10()
{
 int fclc_1;
 fclc_1 = 100;
}

Representing and Handling Native Calls

The opcode CallNative handles all the calls to native functions from inside the script. A native can reference variable memory addresses for easy return, or even return values of its own. I will try and show you the assembly and high level equivalency:

[0x10] Push 0
[0x10] PushF -3000.0
[0x15] PushF -3000.0
[0x1A] PushF 0.0
[0x1F] Push 0
[0x20] LocalVar
[0x21] CallNative CREATE_PLAYER 5 0 ; 5 parameters in CREATE_PLAYER and no return value
[0x28] Push 0
[0x29] LocalVar
[0x30] RefGet
[0x31] Push 1
[0x32] LocalVar
[0x33] CallNative GET_PLAYER_CHAR 2 0 ; 2 parameters in GET_PLAYER_CHAR and no return value
[0x3A] Push 1
[0x3B] LocalVar
[0x3C] RefGet
[0x3D] CallNative IS_CHAR_DEAD 1 1 ; 1 parameter in IS_CHAR_DEAD and 1 return value
[0x44] Push 0
[0x45] LocalVarPtr
[0x46] RefSet ; Store the return address from IS_CHAR_DEAD into the first function local
native void CREATE_PLAYER(int modelHash,float x,float y,float z,Player* handle);
native void GET_PLAYER_CHAR(Player handle,Char* handle2);
native bool IS_CHAR_DEAD(Char handle);

local Player pHandle;
local Char cHandle;

void sub_10()
{
 bool deadstore;
 CREATE_PLAYER(0,-3000.0,-3000.0,0.0,&pHandle);
 GET_PLAYER_CHAR(pHandle,&cHandle);
 deadstore = IS_CHAR_DEAD(cHandle);
}

If Statements

If statements rely on 2 basic opcodes, JumpTrue and JumpFalse. Everything in the statement can be explained by IsZero and the Cmp opcodes. And and Or logical operations can be represented by stack manipulation in the assembly. Here is a simple if statement that will execute some code if the statement is true:

[0x10] Push 0
[0x11] GlobalVar
[0x12] RefGet
[0x13] JumpFalse 0x1C
[0x18] Push 0
[0x19] Push 0
[0x1A] GlobalVar
[0x1B] RefSet
[0x1C] FnEnd 0 0

That snippet of assembly will look like this when represented in a high level scope:

global bool testglob = true;

void sub_10()
{
 if(testglob)
 {
  testglob = false;
 }
}

Notice how I use JumpFalse, so if it turns out testglob equals 0 or false, it simply doesn't execute the command testglob = false.

Not

Most languages have support for something called the Not operator, commonly represented with an exclamation mark (!). This will reverse the top of the stack from true to false or false to true. So to use that operator you would use the IsZero opcode, like so:

[0x10] Push 0
[0x11] GlobalVar
[0x12] RefGet
[0x13] IsZero
[0x14] JumpFalse 0x1D
[0x19] Push 0
[0x1A] Push 0
[0x1B] GlobalVar
[0x1C] RefSet
[0x1D] FnEnd 0 0
global bool testglob = true;

void sub_10()
{
 if(!testglob)
 {
  testglob = false;
 }
}

Now if testglob is equal to 0 or false, will the script execute testglob = false.

And

To make an if statement dealing with an And operator you must use the And opcode which will perform the logical operation to see if the top 2 stack frames are true.

[0x10] Push 0
[0x11] GlobalVar
[0x12] RefGet
[0x13] IsZero
[0x14] Push 1
[0x15] GlobalVar
[0x16] RefGet
[0x18] And
[0x19] JumpFalse 0x21
[0x1D] Push 0
[0x1E] Push 0
[0x1F] GlobalVar
[0x20] RefSet
[0x21] FnEnd 0 0
global bool testglob = false;
global bool testglob2 = true;

void sub_10()
{
 if(!testglob && testglob2)
 {
  testglob = false;
 }
}

This will only execute if testglob equals false and testglob2 equals true.

Or

The Or statement is very much like the And statement, requiring just the insertion of an Or opcode. Here is an example with Not, And and Or:

[0x10] Push 0
[0x11] GlobalVar
[0x12] RefGet
[0x13] IsZero
[0x14] Push 1
[0x15] GlobalVar
[0x16] RefGet
[0x18] And
[0x19] Push 2
[0x1A] GlobalVar
[0x1B] RefGet
[0x1C] Or
[0x1D] JumpFalse 0x26
[0x22] Push 0
[0x23] Push 0
[0x24] GlobalVar
[0x25] RefSet
[0x26] FnEnd 0 0
global bool testglob = false;
global bool testglob2 = true;
global bool testglob3 = true;

void sub_10()
{
 if((!testglob && testglob2) || testglob3)
 {
  testglob = false;
 }
}

This will execute testglob = false if testglob equals false and testglob2 equals true or testglob3 equals true.

Switch Statements

Switch statements are like if statements, but they deal with more possibilities than true or false. There is a very simple switch opcode which provides this functionality, so here is the assembly of a switch operation (along with a default case) and the corresponding high level code to go with it:

[0x10] Push 0
[0x11] GlobalVar
[0x12] RefGet
[0x13] Switch 3 [0:0x32] [1:0x3B] [2:0x44]
[0x2D] Jump 0x4D
[0x32] Push 0
[0x33] Push 1
[0x34] GlobalVar
[0x35] RefSet
[0x36] Jump 0x56
[0x3B] Push 1
[0x3C] Push 1
[0x3D] GlobalVar
[0x3E] RefSet
[0x3F] Jump 0x56
[0x44] Push 2
[0x45] Push 1
[0x46] GlobalVar
[0x47] RefSet
[0x48] Jump 0x56
[0x4D] Push 3
[0x4E] Push 1
[0x4F] GlobalVar
[0x50] RefSet
[0x56] FnEnd 0 0
global int testglob1 = 1;
global int testglob2 = 0;

void sub_10()
{
 switch(testglob1)
 {
  case 0:
   testglob2 = 0;
   break;
  case 1:
   testglob2 = 1;
   break;
  case 2:
   testglob2 = 2;
   break;
  default:
   testglob2 = 3;
   break;
 }
}

Tools

  • GTA Net GTAForums: Scone – SCO (Dis-)assembler by Sacky
  • OpenIV – contains a built-in decompiler
  • SparkIV – contains a built-in decompiler