Skip to main content

GSC syntax & debugging

Learn the syntax for GSC including basic examples and notes to help you.

Comments

Comments exist in two ways, one-line, or multi-line block comments.

// This is a one-line comment.

/*
This is a comment across
multiple lines!
*/

Function Declaration

Declaring functions in GSC require you to give it a name, followed by a () and a bracket scope.

my_function()
{ // start of bracket scope
// [...]
} // end of bracket scope

Calling functions

GSC functions can be "threaded" or called sequentially (in logical order/sequence). If a function is "threaded", then the execution of the function being called will not hold the execution of the parent function that contains the call.

my_function()
{
print("my_other_function execution starting");
my_other_function(); // my_function() will not continue past this line until the execution of my_other_function() finishes
print("my_other_function execution completed");

print("my_other_function thread execution starting");
thread my_other_function(); // my_function() will continue past this line and carry on doing other stuff
print("my_other_function thread execution still continuing, but it's threaded");
}

my_other_function()
{
wait 3; // this function executes a 3 second wait
print("3 seconds waited");
}

/*
"my_other_function execution starting"
"3 seconds waited"
"my_other_function execution completed"
"my_other_function thread execution starting"
"my_other_function thread execution still continuing, but it's threaded"
"3 seconds waited"
*/

If you call a function on an entity, the self keyword is reversed as the entity who called the function

my_function()
{
getplayers()[0] hello_world(); // index 0 will get the first player in the game
}

hello_world() // self == player
{
self iprintln("hello world!");
}

Using variables

Variables are used to store data for the span of the current game. Variables will reset upon map rotation unless stored outside of the GSC VM context. However, for round-based gamemodes, persistent variables will keep the data through restarts.

A local variable is a variable that can be used in the whole function scope it's defined in. A variable can b e used to call a function or passed to a function via parameters. Defining a local variable is as easy as:

variable = 1;

Variables can also be global:

level.variable = 1;

or defined on an object (or entity) and be passed through to functions:

my_function()
{
variable = spawnstruct();
variable.message = "hi!";

variable print_message();
}

print_message() // self == variable
{
print(self.message);
}

/*
"hi!"
*/

Math

Math can play a huge part in gameplay scripting depending on what you are doing. The operators you can use for math in GSC include the following:

+       Addition
- Subtraction
* Multiplication
/ Division
% Modulus (Remainder)
= Equals
++ Increment (+1)
-- Decrement (-1)
+= Incrementation (requires number)
-= Decrementation (requires number)

Example some of the operations:

var++;
var--;
var += int;
var -= int;

You can also use Bitwise (Wikipedia) operations in GSC.

&       Bitwise And
| Bitwise Or (inclusive or)
^ Bitwise Xor (exclusive or)
<< Left Shift
>> Right Shift
~ Bitwise NOT (one's complement)

If statements

An 'if' statement is used to see if a condition is met, and then execute code (delared in a scope) as the result. The operators you can use for comparing data in GSC include the following:

==      Equal To
!= Not Equal To
! Negation (Not equal to)
< Less than
> Greater than
<= Less or Equal to
>= Greater or Equal to
&& And
|| Or

Example:

if (9 > 10)
{
print("9 is above 10");
}
else
{
print("9 is below 10");
}

/*
"9 is below 10"
*/

You can also do if statements on any sort of variables

message = "hi!";

if (message == "hi!")
{
print("hello variable!");
}
else
{
print("variable didn't say hi? :(");
}

/*
"hello variable!"
*/

The conditions can also have a "else if" to check multiple conditions

big_number = 100;

if (big_number < 20)
{
print("big number is below the number 20");
}
else if (big_number < 150)
{
print("big number is below the number 150");
}
else
{
print("big number is above the number 150");
}

/*
"big number is below the number 150"
*/

In GSC, you can check if a variable is defined with a value. This is because if a variable was undefined, and you check if it's true, it'll invoke on the condition. To do this, use the function isdefined(var), which returns true/false. If a variable equals keyword undefined or isn't defined at all, you should get false as the return type.

variable_one = 5;
variable_two = 6;

if (isdefined(variable_one))
{
print("variable_one is defined!");
variable_two = undefined; // undefined is a reserved keyword
}

if (variable_two) // the variable is undefined, but it'll still think this condition as met
{
print("variable_two claimed to be true, but is it defined?");
if (isdefined(variable_two))
{
print("is not defined");
}
else
{
print("is defined");
}
}

/*
"variable_one is defined!"
"variable_two claimed to be true, but is it defined?"
"is not defined"
*/

This is why the GSC scripts will check if a variable isdefined before checking if it is true:

if (isdefined(variable_two) && variable_two)
{
print("variable_two is defined and is true");
}

/*
"variable_two is defined and is true"
*/

In GSC, the ternary operator is also supported.

condition = (5 > 2) ? true : false;
print(condition);

/*
"true"
*/

Loops

Loops in GSC can be somewhat similar to C, but may come in different forms.

  • A while loop is a loop that will keep looping while the condition provided is true
  • A for loop is a loop that loops a set amount of times
  • A foreach loop is a simplified for loop meant to be used on all items of an array.

while

In this example, we will check if number X is above 10.

x = 0;
while (x < 6)
{
print(va("x = %s", x));
number++;
}

print("while loop condition was met");

/*
"x = 0"
"x = 1"
"x = 2"
"x = 3"
"x = 4"
"x = 5"
"x = 6"
"while loop condition was met
*/

for

In this example, we will create an infinite loop that will never end.

my_function()
{
thread infinite_loop(); // make sure the loop you are doing doesn't halt the parent function
}

infinite_loop()
{
for(;;)
{
print("infinite loop");
wait 0.05; // infinite loops NEED to wait a server frame else your script will lag terribly or your game will crash.
}
}

foreach

In this example, we will create a loop that prints every single player in the game's name.

foreach (player in getplayers())
{
print(va("player: %s", player.name));
}

The for loop equivalent to this would be:

players = getplayers();
for (i = 0; i < players.size; i++)
{
print(va("player %s: %s", i, players[i].name));
}

Wait

GSC is ran on every server frame, and 20 server frames equal to 1 second.

wait 0.05;  // 1 server frame
wait 0.5; // 10 server frames
wait 1; // 20 server frames
wait (1); // 20 server frames

Switch

Switch cases are useful for checking the case of many possibly values. This is usually recommended if you are have more than 4-5 variables as it may be faster and more organized compared to a if statement.

value = 5;
switch (value)
{
case 1:
print("value is 1");
break;
case 2:
print("value is 2");
break;
case 3:
print("value is 3");
break;
case 4:
print("value is 4");
break;
case 5:
print("value is 5");
break;
default:
print("value is not any of the cases listed");
break;
}

Events

notify

A notify event will send a cue to a endon or waittill to do something. When you notify something, it is called like <entity> notify("my_cool_notify").

endon

Knowing how a notify works, you can end threads you create easily. One of the most common notifies that occur is when a player disconnects from the game, which should be used in any player thread you run.

my_cool_player_func() // self == player
{
self endon("disconnect"); // ends when player disconnects
level endon("game_ended"); // ends when level ends game

for(;;)
{
print(va("player %s has %s kills!", self.name, self.kills));
wait 5;
}
}

waittill

A notify can also execute a waittill. A waittill is something that waits for a event to be notified, and then you can execute code off of it. If you run a waittill that isn't in a for or while, you still need a endon because the thread will never end or cause undefined behavior if so.

on_player_connect() // self == level
{
level endon("game_ended"); // ends when level ends game

for(;;)
{
level waittill("connected", player); // this is called every time a player connects
print(va("player %s has connected!", player.name));
}
}

TODO

Custom functions

With H1-mod, we add a variety of custom GSC functions and methods that are unique to the client and can assist in GSC. Below are most of the functions documented for usage.

Misc

  • print(message)

  • println(message): Prints a message to the console.

  • logprint(message): Prints a message to the game log.

  • toupper(string): Converts all of a string to uppercase.

  • executecommand(command): Executes a console command.

  • replaceFunc(with, what): Replaces a script function with another script function.

    main()
    {
    replacefunc(maps\mp\gametypes\_callbacksetup::callbackVoid, ::callbackvoid_override);
    }

    callbackvoid_override()
    {
    print("callback void override was called");
    }
  • getFunction(filename, name): Gets a function from a GSC script.

    init()
    {
    function = getFunction("maps/mp/gametypes/_callbacksetup", "callbackVoid");
    [[ function ]]();
    }
  • getFunctionName(function): Returns the function's name.

    init()
    {
    function = getFunction("maps/mp/gametypes/_callbacksetup", "callbackVoid");
    print(getFunctionName(function)); // "maps/mp/gametypes/_callbacksetup::callbackVoid"
    }
  • say(message): Sends a chat message to all players.

    onPlayerConnected()
    {
    while (true)
    {
    level waittill("connected", player);
    say(player.name + " connected!");
    }
    }
  • entity tell(message): Sends a chat message to a player.

    onPlayerConnected()
    {
    while (true)
    {
    level waittill("connected", player);
    player tell("Welcome back " + player.name + "!");
    }
    }
  • va(string, ...): Format arguments via a format string.

    init()
    {
    server_owner = "mikey";
    server_name = "Epic Server";
    va_string = va("Welcome to %s's %s!", server_owner, server_name);
    print(va_string); // "Welcome to mikey's Epic Server!"
    }

IO

warning

IO in GSC, by default, is only used when the fs_game dvar is applied to a valid directory within the game. When the fs_game dvar is defined, IO is locked into that directory and cannot leave that directory.

However, you can opt-in to allow IO in GSC to use your root game directory folder instead. To do so, launch the game with the -allow_root_io command line argument. We DO NOT recommend using this flag or enabling it for 3rd-party mods.

  • fileExists(path): Returns true if the file exists.
  • writeFile(path, data[, append]): Creates a file if it doesn't exist and writes/appends text to it.
  • readFile(path): Reads a file.
  • fileSize(path): Returns file size in bytes.
  • createDirectory(path): Creates a directory.
  • directoryExists(path): Returns true if the directory exists.
  • directoryIsEmpty(path): Returns true if the directory is empty.
  • listFiles(path): Returns the list of files in the directory as an array.
  • copyFolder(source, target): Copies a folder.
  • removeFile(path): Deletes a file if it exists.

JSON

  • jsonSerialize(variable[, indent]): Converts GSC variables (such as arrays) into JSON.

    init()
    {
    array = [];
    array[0] = 1;
    array[1] = 2;
    json = jsonSerialize(array, 4);

    print(json);

    /*
    [
    2,
    1
    ]
    */
    }

    This function can also be useful to reveal contents of existing arrays such as game:

    init()
    {
    print(jsonSerialize(game["round_end"]), 4);

    /*
    {
    "draw": 1,
    "round_draw": 2,
    "round_win": 3,
    "round_loss": 4,
    "victory": 5,
    "defeat": 6,
    "halftime": 7,
    "overtime": 8,
    "roundend": 9,
    "intermission": 10,
    "side_switch": 11,
    "match_bonus": 12,
    "tie": 13,
    "game_end": 14,
    "spectator": 15,
    }
    */
    }
  • jsonParse(json): Converts JSON into a GSC variable.

    init()
    {
    array = jsonParse("[1, 2, 3, 4]");
    print(array[0] + " " + array[1] + " " + array[2] + " " + array[3]);

    /*
    1 2 3 4
    */
    }
  • jsonPrint(...): Prints values as json.

  • map(...): Creates a string-indexed array.

    init()
    {
    array = map("first", 1, "second", 2);
    print(jsonSerialize(array, 4));
    /*
    {
    "first": 1,
    "second": 2
    }
    */
    }

Debugging

  • assert(condition):

  • assertex(condition, error): Throws an error if condition is false.

  • type(variable)

  • typeof(variable): Returns the name of the type of variable.

Script errors

GSC is a scripting language that is prone to errors very easily. When undefined behavior occurs, it'll just keep going the best it can. For example, when a variable is undefined but used in a if statement, it'll run the code as if the variable was true. This runtime error below shows the instruction that is failing, and then the function & file.

runtime error example

For some reason, it's not showing the name & function it's erroring in while I write this up, but the line information is correct and we can break it down from there. So using the information, we can conclude the variable at line 50 is undefined.

runtime error code

Stuff like this makes people think their mod is working because they don't get runtime errors normally, but in reality, it's just working by luck. In H1-Mod, we restore a dvar known as developer_script. When you set this dvar to 1, and start or map_restart a game, you will get debug information about the script including runtime errors that'll show errors in your script.

Opcodes

Some of the opcode instructions that show might be confusing, but they're usually straightforward. Below is a list of very common opcodes and how to fix them. The common theme between all of these is that a variable being checked is undefined, therefore needs a definition somewhere or you need logic to check if isdefined before you do stuff with it.

OpcodeReason
OP_EvalSelfFieldVariableTrying to access a field like self.bruh that might be undefined
OP_SetLevelFieldVariableFieldTrying to set a field on the level object
OP_CastBoolA variable being casted to bool is undefined
OP_JumpOnFalseIf condition failing, usually commonly a undefined variable.
OP_JumpOnTrueSame as OP_JumpOnFalse
OP_ScriptMethodThreadCallPointerMost common reason is the caller is undefined
OP_CallBuiltinMethodA builtin engine method is failing due to bad parameters
OP_inequalityA != is failing, meaning one or both of the variables might be undefined
OP_equalityA == is failing, meaning one or both of the variables might be undefined
OP_greaterA > is failing, meaning one or both of the variables might be undefined
OP_greater_equalA >= is failing, meaning one or both of the variables might be undefined
OP_lessA < is failing, meaning one or both of the variables might be undefined
OP_less_equalA <= is failing, meaning one or both of the variables might be undefined
OP_incA variable += 1 or variable++ is failing, meaning the variable is undefined
OP_plusA + is failing, meaning one or both of the variables might be undefined
OP_divideA / is failing, meaning one or both of the variables might be undefined
OP_multiplyA * is failing, meaning one or both of the variables might be undefined
OP_GetSelfObjectSelf is undefined, this should never happen
OP_SetVariableFieldSetting a field on a variable that's not a entity is failing. Variable may be undefined
OP_endonVariable being used on a endon is undefined
OP_notifyVariable being used on a notify is undefined
OP_waittillVariable being used on a waittill is undefined
OP_waitTime for a wait is failing, if variable is used probably undefined
OP_EvalArrayTrying to do variable[index] fails, variable might be undefined
OP_switchThe variable for the switch case table is undefined
OP_sizeDoing variable.size fails due to variable being undefined
OP_checkclearparamsToo many parameters might be getting passed to the function, or not the required ones
OP_BoolNotDoing a inverse of variable like variable = !variable meaning undefined variable

Variable leaks

Variable leaks are when so much undefined behavior is happening that the game starts leaking variables. The GSC VM only has so many variables it can allocate, and for stability on a dedicated server, you must write good code. This can happen for various reasons, including more than just undefined variables. Threads that run waittill, for loops, or even while loops per player or level can easily leak if you are not properly ending them when the game ends, or when exitlevel is called.

The errors that occurs will be something along the lines of "child/parent variables maxed out". For example, this function below shows a function calls by a player that waits until the game is over, and then runs some code.

waittill_game_end() // self == player
{
//self endon("disconnect"); // let's act like our code doesn't have this
level endon("shutdownGame_called"); // absolute last notify to end threads

level waittill("game_ended");

print(va("%s has %s kills when game is ended", self.name, self.kills));
}

This function may look alright, but there are already some issues that can occur. If a player leaves the game, this code will still run. When that happens, the game will cry about "removed entity" errors because the player no longer exists. You now have a variable leak! To fix this, you add a self endon("disconnect"); so the thread ends when a player leaves.

Each function you create should always include a endon for game_ended, or shutdownGame_called if you need to end it at the very last second of the match. If it's a player thread, it should always endon disconnect for the player entity.