First native support for functions in GTA was added in Liberty City Stories. The games prior to that only had limited subroutines invoked with the gosub
command. Those subroutines allowed to avoid code duplication; however, they were operating on the same scope as the code invoking them. Any variables used in a subroutine would alter the state of the script.
CLEO Library brought support for functions (dubbed scm func) in early versions, and it became important part of the modern scripting techniques. Each SCM function owns 16
or 32
local variables (depending on the game), none of them would clash with the local variables of the script calling the functions. CLEO5 improved SCM functions a lot by completely isolating script and function state, adding support for string arguments, and more.
Sanny Builder 4 adds new syntactic element to the language to easily create and use SCM functions in the code.
{% hint style="info" %} Visit this GitHub ticket for more technical information and examples on function syntax. {% endhint %}
To make a new function, use the function
keyword.
function <signature>
<body>
end
signature
defines function's input parameters and their type, and also an optional return type.
body
is the code that runs when you call the function.
A function must end with the end
keyword.
Functions are available anywhere in the current scope. Functions defined inside other functions only available in that function.
function sum(a: int, b: int): int
int result = a + b
return result
end
A function's signature defines what types of input arguments the function receives and what type of value it returns. Function arguments may be of a primitive type int
, float, string
, or a class, e.g. Car
or Pickup
.
{% hint style="warning" %} String arguments are only supported in CLEO 5. They are always passed as pointers. {% endhint %}
A function may have zero parameters. If it has parameters, they are listed between ()
. Each parameter has a name and a type, separated by a colon. Parameter declaration syntax is similar to that of var..end
. Each parameter can be used as a function's local variable in the function body.
function loadModel(modelId: int)
request_model modelId
while not is_model_available modelId
wait 0
end
end
modelId
is an input parameter of the int
type. It can be used as a local variable.
If the function returns something, its type has to be defined after the list of parameters (or the function name, if there are no parameters). E.g.:
function foo: float
function bar(i: int): int
Functions can be called using its name followed by open and closed braces:
function foo
end
foo()
()
are required for the function call, even if function receives no arguments. Without ()
function name is compiled as it's offset or address (if this is a static foreign function):
function foo
end
jump foo // jumps into the function body
A function's body include all instructions executed when the main code calls the function. The function may have zero instructions. Function parameters can be referenced in the body as local variables. The function may create extra local variables and even new functions, available only within this function:
int x
function mod
int x = 5 // this `x` only exists within function `mod`
end
x = 10
mod()
// x is still equal to 10
Function ends at the end
keyword. You may exit early using the return
keyword.
{% hint style="warning" %}
Exit from a function using return
keyword is only supported in CLEO 5 (San Andreas). For other CLEO versions use cleo_return
command.
{% endhint %}
function SetWantedLevel(level: int)
set_max_wanted_level level
end // no explicit return, function ends here
function SetWantedLevel(level: int)
if or
level < 0
level > 6
then
return // early return, code below won't be executed
end
set_max_wanted_level level
end // function ends here
For a function to be used as a condition in IF..THEN
, it must have a special return type: logical
.
function check(): logical
if check()
then
end
logical
function returns a result of logical expression, or true
or false
.
return true
return false
return 0@ == 1
return Char.DoesExist(0@)
A value returned from a logical function sets the script's condition result and be combined with other checks in one IF statement.
function isDefined(val: int): logical
if val <> 0
then return true
else return false
end
end
if isDefined(5)
then
// result is true
else
// result is false
end
{% hint style="info" %} The example above can also be written in a more concise way:
function isDefined(val: int)
return val <> 0
end
{% endhint %}
The function may return one or multiple values, using the return
keyword.
To define a function that returns something, add a colon and a type at the end of the function signature:
function maxItems: int
To return a value use return
followed a value:
return 5
To read the returned value, a caller must provide a variable:
int value = maxItems() // value is 5
{% hint style="info" %} Returning any value, even 0, from a function is considered a success if the function is used as a condition.
function zero: int
return 0
end
if
zero()
then
// success
else
// will never be here
end
{% endhint %}
If a function may fail and not have a valid result, its return type should be marked as optional
and a blank return can be used:
function createCar: optional int
return
end
if
Car c = createCar()
then
// got a car handle in c
else
// got nothing
end
optional
keyword must precede the list of return types.
Some functions may not be able to return correct values (a fallible function). For example, a function reading a file may fail if the file does not exist. In this case the return type can be marked with the optional
keyword:
function getValues: optional int, int, int
if <...>
then
return 1 2 3
else
return
end
end
Function getValues
may return 3
integer values or nothing. On calling end, to check whether a fallible function succeeded it can be wrapped into IF..THEN condition like so:
int a, b, c
if
a, b, c = getValues()
then
// we got 3 values in a, b, c
else
// we got nothing, a, b, c have not been changed
end
CLEO provides an interface for calling game's native functions. There are 4 opcodes that support different calling conventions:
0AA5: call_function {address} [int] {numParams} [int] {pop} [int] {funcParams} [arguments]
0AA6: call_method {address} [int] {struct} [int] {numParams} [int] {pop} [int] {funcParams} [arguments]
0AA7: call_function_return {address} [int] {numParams} [int] {pop} [int] {funcParams} [arguments] {var_funcRet} [var any]
0AA8: call_method_return {address} [int] {struct} [int] {numParams} [int] {pop} [int] {funcParams} [arguments] {var_funcRet} [var any]
As you may guess, using them directly is not very convenient. These opcodes have their own quirks, like having to provide input arguments in reverse order.
Sanny Builder 4 offers an interface for defining foreign functions in code and using them as regular functions. It can be done by adding calling convention type to a forward declaration.
function<cc[,address]>(args): return type
A cc
or calling convention defines who's in charge of cleaning up the stack when function returns.
CLEO and Sanny Builder supports 3 major conventions:
- cdecl - caller cleans up the stack
- stdcall - callee cleans up the stack
- thiscall - callee cleans up the stack. Since
thiscall
is a class method function, it additionally receives a pointer to the class instance inecx
register.
You can read more about different types of calling conventions on Wikipedia.
Optional address parameter defines where this function is located in the game memory (static functions). If this address can only be known in runtime, this parameter can be omitted.
A return type can be int
, float,
or string
.
function CStats__GetStatType<cdecl,0x558E30>(statId: int): int
int type = CStats__GetStatType(42)
// 0AA7: call_function_return {address} 0x558E30 {numParams} 1 {pop} 1 {funcParams} 42 {var_funcRet} 0@
This code invokes a function at address 0x558E30
with argument 42
and stores the returned value in the variable type
.
Passing multiple arguments can be done as usual:
function Foo<stdcall,0x400000>(int, float): int
int value = Foo(10, 20.0)
// 0AA7: call_function_return {address} 0x400000 {numParams} 2 {pop} 0 {funcParams} 20.0 {var_funcRet} 10 0@
Calling a thiscall
function requires the first argument to always be a pointer to the class instance.
function Destroy<thiscall,0x400000>(struct: int)
int instance = 0xDEADD0D0
Destroy(instance)
// 0006: 0@ = 0xDEADD0D0
// 0AA6: call_method {address} 0x400000 {struct} 0@ {numParams} 0 {pop} 0
When function's address is not known at compile time, you still can define a foreign function and use a function pointer to call it by reference. To declare a function pointer, declare a new variable with the function name as the type:
function Destroy<thiscall>(struct: int)
Destroy method // define a pointer to function Destroy
...
method = 0x400000 // function is located at 0x400000
method(0xDEADD0D0) // call function using the pointer
// 0006: 0@ = 0x400000
// 0AA6: call_method {address} 0@ {struct} 0xDEADD0D0 {numParams} 0 {pop} 0
{% hint style="info" %}
Static function's name represents its address when used without ()
function Foo<stdcall,0x400000>(int, float): int
int addr = foo // addr = 0x400000
{% endhint %}