United Offensive scripting language

Status and authority

This document specifies the multiplayer scripting language implemented by the stock Call of Duty: United Offensive compiler and virtual machine. The project’s recovered STRICT_STOCK source is the exclusive authority for the syntax and behavior described here. No online GSC reference, later Call of Duty dialect, or compatibility extension is used as a source.

The specification follows the recovered scanner and parser tables, AST builders, bytecode compiler, VM, value and variable systems, notification runtime, animation-tree loader, and stock game-module registries. Maintained non-stock safety checks and compatibility behavior are excluded. A behavior which the stock source does not establish is not generalized from another engine.

The language and the game API are separate layers:

Quick example

main()
{
    level.round_started = true;
    level thread monitor_round_end();
}

monitor_round_end()
{
    level endon("intermission");

    while (level.round_started)
    {
        level waittill("player_scored", player, points);
        player iprintln("score: " + points);
    }
}

Source files and program structure

Script source files use the .gsc extension. The loader accepts a logical name without the extension and appends .gsc. It canonicalizes logical names by lowercasing them, converting forward slashes to backslashes, and collapsing leading or repeated path separators.

A source file contains only top-level function definitions, #using_animtree directives, and developer-only function definitions. It does not execute top-level statements.

#using_animtree("american_soldier");

main(arg1, arg2)
{
    // statements
}

The compiler records every function before emitting any body. Functions may therefore call functions defined later in the same file. Duplicate function names are compile errors. An unresolved local or external function is also a compile error.

Names in another script use a backslash-separated logical path and :::

maps\mp\gametypes\_callbacksetup::codecallback_startgametype();
callback = maps\mp\example::handler;

The first form calls a function. The second stores a function value. A cross-file reference schedules that script for loading after the current file has been compiled.

Lexical structure

Character and case rules

The recovered scanner operates on bytes. Identifiers consist of letters, digits, and underscores, and may begin with an underscore. A token beginning with a digit is scanned as a number first; 1name is therefore not one identifier.

Identifiers, function names, field names, and script-path components are interned in lowercase. Keyword recognition itself is case-sensitive and the keyword spellings are lowercase. Use lowercase source spelling consistently; an uppercase spelling can be scanned as an identifier in a position where the parser requires a keyword.

The reserved words are:

return wait thread undefined self level game anim
if else while for do switch case default break continue
waittill waittillmatch notify endon false true

Whitespace and comments

Spaces, tabs, and line breaks separate tokens and are otherwise ignored. Ordinary comments use either form:

// to the end of the line
/* across one or more lines */

/# and #/ are not ordinary comment tokens. They delimit compilable developer-only code and have the semantics described in Developer-only code.

Strings

An ordinary string uses double quotes. Prefixing the opening quote with & creates a localized string value.

text = "ordinary";
localized = &"MENU_OK";

The scanner recognizes \n, \r, and \t. For every other escape, the backslash is discarded and the following character is retained. For example, \" produces a quote and \\ produces one backslash.

Ordinary and localized strings are distinct VM types. The explicit (string) cast does not convert a localized string to an ordinary string.

Numbers

Integer literals are decimal. Hexadecimal notation is not part of this scanner: 0x10 is tokenized as integer 0 followed by an identifier.

Float literals use a decimal point, for example 0.5, .5, or 1.25. The source scanner does not recognize exponent notation. A trailing point is not a float token, so 1. is invalid as a single numeric literal.

A minus immediately before an integer or float literal forms a negative numeric literal. General unary negation is absent: -1 and -0.5 are valid, but -value is not. Unary plus is also absent.

Exact composite tokens

The following spellings are each one scanner token:

(bool)  (int)  (float)  (string)
.size
#using_animtree
#animtree
::

Their internal whitespace is significant. (int)value is a cast, while ( int )value does not parse as one. Likewise, array.size is the size operator, while array . size is an ordinary field access named size.

Values and types

Script-visible values

Type

Meaning

undefined

Absence of a value. Unset locals, missing array entries, omitted parameters, and functions without a value result use this type.

int

Signed 32-bit integer payload. false is integer zero and true is integer one.

float

IEEE-754 binary32 value.

string

Interned ordinary string.

localized string

Interned localized-message string written with &"...".

vector

Three binary32 components.

object

Reference to an entity, struct, array, thread object, or another field-bearing VM object.

function

Reference to compiled script code.

animation

Animation index paired with its animation-tree index.

The VM also has implementation types named codepos, key/value, stack, thread, entity, struct, array, dead thread, dead entity, and dead object. Object subtypes are usually carried to script as an object value; code positions, archived stacks, and dead variants are VM bookkeeping rather than source literals.

Literal and predefined values

Source form

Result

undefined

Undefined value.

false / true

Integer zero / integer one.

"text"

Ordinary string.

&"text"

Localized string.

(x, y, z)

Vector; each of the three values is cast to float at runtime.

[]

New empty array.

%animation_name

Animation reference resolved through the file’s active animtree.

#animtree

Integer handle of the file’s active animtree.

::function

Same-file function value.

path\file::function

Cross-file function value.

(expression) is grouping when it contains one expression. A parenthesized list must contain exactly one or three expressions; the three-expression form constructs a vector.

Predefined objects

self is the parent object of the current thread. A normal function call without an explicit method object inherits the caller’s self. A method call supplies its object as the callee’s self.

level and game are distinct global field-bearing objects created when the script system starts. anim is the global animation array value used by the script runtime. Their fields persist independently of function-local variables for the life of the script system.

Variables, objects, and arrays

References

The language has local variables, object fields, and indexed references:

local = 1;
level.phase = "playing";
values[0] = "first";
values["name"] = "value";

Dot access and string indexing address the same name domain on arrays and field objects where that access is supported. Indexed assignment to an undefined local creates an array automatically.

Array keys may be ordinary strings or integers. The valid stock integer-key domain is -8257536 through 8388607. Reading a missing array entry produces undefined. Assigning and clearing arrays use copy-on-write when the array is shared; nested arrays are copied recursively while non-array objects remain shared references.

Strings and vectors support read-only integer indexing:

  • string[index] returns a one-character ordinary string and requires 0 <= index < strlen(string);

  • vector[index] returns a float and accepts only indexes 0, 1, and 2;

  • individual string characters and vector components cannot be changed.

Size

The exact .size token applies these rules:

Operand

Result

array object

Number of array entries.

other object

Integer 1.

ordinary string

Byte length before the terminating NUL.

any other type

Runtime error. In particular, localized strings are not accepted.

Assignment and mutation

Simple assignment uses =. Compound assignment supports:

|=  ^=  &=  <<=  >>=  +=  -=  *=  /=  %=

The compound form reads the old value, applies the corresponding binary operator, and writes the result. reference++ and reference-- are statements, not general postfix expressions, and require an integer value. They wrap in the same 32-bit domain as integer addition and subtraction.

Expressions

Operator precedence

Binary operators are left-associative. The table runs from lowest to highest precedence.

Level

Operators

Notes

1

||

Short-circuit logical OR.

2

&&

Short-circuit logical AND.

3

|

Integer bitwise OR.

4

^

Integer bitwise XOR.

5

&

Integer bitwise AND.

6

== !=

Equality and inequality.

7

< > <= >=

Numeric relational comparison.

8

<< >>

Integer shifts.

9

+ -

Addition, subtraction, vector arithmetic, or concatenation as described below.

10

* / %

Numeric multiplication, division, and remainder.

11

! ~ and exact cast tokens

Unary logical not, integer bitwise not, and explicit casts.

Indexing, field access, .size, function selection, and calls bind more tightly than the operators in the table. The language has no conditional ?: operator and no general unary + or - operator.

Truth conversion and logical operators

Conditions accept integers directly. Floats are false only when equal to zero. Ordinary strings are converted with the stock decimal integer parser; a zero result is accepted only when the trimmed, optional-sign-prefixed text starts with 0 or .0. Other types cannot be used as conditions.

! returns normalized integer zero or one. && and || short-circuit. The right operand is converted to normalized boolean when evaluated. If the left operand short-circuits and is already an integer, its original nonzero or zero integer payload remains the expression result; do not assume that every logical expression produces exactly one or zero.

Explicit casts

Cast

Accepted inputs

(bool)

Int, float, or ordinary string. The result is integer 0 or 1.

(int)

Int, float, or ordinary string. Float conversion truncates according to the stock target conversion; strings use the stock decimal parser.

(float)

Float, int, or ordinary string. The result is binary32.

(string)

Ordinary string, int, float, or vector.

The string-to-number casts use the same zero-text rule as truth conversion. They accept leading whitespace and an optional sign through the stock C conversion routines. Windows and Linux stock builds differ for an extremely small nonzero string value which converts to a nonzero double but narrows to float zero: Windows accepts it, while Linux tests the narrowed float and rejects it unless the text has a zero-literal prefix.

Implicit pair conversion

Binary operators first attempt the stock pair conversion:

  • int is promoted to float when paired with float;

  • int, float, or vector is converted to an ordinary string when paired with an ordinary string;

  • otherwise, unlike types produce an unmatching types runtime error.

This conversion precedes operator validation. It enables string concatenation such as "score: " + score, but it does not make subtraction or relational comparison valid for strings.

Arithmetic and bitwise behavior

Operand type

Operations

int

+ - * / % and all bitwise/shift operators. Addition, subtraction, and multiplication wrap in 32 bits. Division and remainder use signed integer semantics and reject zero divisors.

float

+ - * /. Float remainder is not supported; division rejects a zero divisor according to the stock comparison.

vector

Vector + and - only, component by component.

ordinary string

+ only, producing an interned concatenated string.

Shift counts are masked with 31. Left shift operates on the 32-bit bit pattern. Right shift is an arithmetic signed shift. Stock signed integer division of -2147483648 / -1 reaches the target CPU’s overflowing divide instruction; it is not a defined, recoverable script result.

Equality and comparison

Equality is defined after implicit pair conversion:

  • two undefined values are equal;

  • ordinary and localized strings compare their interned handles within their respective type;

  • vectors compare all three components exactly;

  • floats are equal when the absolute difference is smaller than the stock epsilon (approximately 1e-6);

  • integers compare their complete 32-bit payload;

  • objects compare identity;

  • animations compare their packed tree/index identity.

Relational operators accept only int or float after int-to-float promotion.

Functions and calls

Definitions and parameters

add(left, right)
{
    return left + right;
}

Parameters and locals belong to the executing function thread. Extra arguments cause function called with too many parameters when the callee finishes binding its formal parameters. Missing parameters remain undefined. return expression; supplies a value; return; and falling off the function body return undefined.

The stock VM permits 32 nested script-call frames. The compiler also rejects a function when 32 * maximum_local_depth + maximum_operand_stack_depth is greater than 2047. Built-in argument counts are encoded in one byte; the stock compiler rejects counts greater than or equal to 256, making 255 the actual maximum despite its misleading exceeds 256 diagnostic.

Call forms

Form

Meaning

function(args)

Call a named same-file script function or a stock built-in function.

path\file::function(args)

Call a named function in another script.

object function(args)

Method-context call; object becomes the callee’s self. A name found in the stock method registry invokes that built-in method.

[[function_value]](args)

Call through a function value.

object [[function_value]](args)

Call through a function value with explicit self.

thread function(args)

Execute the named function as a new child thread using inherited self.

object thread function(args)

Execute a child thread with explicit self.

The thread prefix also works with the [[function_value]] call forms. A new thread begins executing immediately and runs until it returns, waits, or errors. Its expression result is the immediate child execution result when it returns before suspension, otherwise undefined; it is not a stable source- level thread-handle API. Call statements discard their result.

Control flow

Conditional and loop statements

The stock statements are conventional in shape:

if (condition)
    statement;
else
    statement;

while (condition)
    statement;

do
    statement;
while (condition);

for (initialization; condition; increment)
    statement;

Any for clause may be empty. An empty condition is compiled as true. break is legal in loops and switches. continue is legal in loops. The runtime has a stock loop watchdog which reports or terminates a potential infinite loop according to developer settings.

Switch

switch (value)
{
case 1:
    action();
    break;
case "name":
    other_action();
    break;
default:
    fallback();
    break;
}

Case labels must be integer or ordinary-string literals. Integer cases use the same stock key domain as integer array indexes. Duplicate case values or more than one default are compile errors. Execution falls through unless terminated with break or another control transfer.

Threads, time, and events

Wait

wait expression; converts the expression to float seconds, multiplies by 1000, rounds it with the stock fast-round operation, and schedules the thread on a 24-bit millisecond time key. Negative values and NaN are errors. Values greater than or equal to 16777 seconds are rejected. A wait archives the thread’s live stack and locals and resumes it when that time bucket runs.

wait 0; still yields through the scheduler; it is not equivalent to omitting the statement.

Notify and waittill

Events are attached to field-bearing objects and named by ordinary strings:

level notify("started", player, mode);
level waittill("started", notified_player, notified_mode);

For notify, the first argument must evaluate to an ordinary string. Any remaining expressions are event arguments.

For waittill, the first argument is the event-name expression. Remaining items are local variable names, not input expressions. When a matching notify arrives, its arguments are appended to the archived frame and assigned to those locals in order. The wait registers on the receiver object and event name, then suspends the current thread.

waittill is one-shot: resumption removes that waiter from the object’s event bucket. One notify may wake every waiter registered for that object and name.

Waittillmatch

level waittillmatch("state_changed", expected_player, expected_state);

waittillmatch evaluates and archives its expressions after the event name. On notify, those saved values are compared with the notification arguments in order using normal script equality. The thread resumes only if all saved values match. Unlike waittill, these items are match expressions and are not output local names.

The stock bytecode stores the match count in one byte and the dispatcher sign-extends that byte. The stock compiler does not enforce a safe signed-positive count. Programs must keep the match count within 0 through 127; larger counts enter the stock VM’s invalid frame-walk behavior rather than defining additional language semantics.

Endon

self endon("disconnect");

endon requires an object receiver and an ordinary-string event name. It registers an auxiliary waiter tied to the current thread. A matching notify terminates that thread, including an archived thread suspended in wait or waittill.

Animation trees

#using_animtree is a top-level directive and its argument must be an identifier string:

#using_animtree("american_soldier");

The selected tree is reset for each source file. Animation literals and #animtree require that the file has selected a tree. %run resolves an animation named run in that tree and emits an animation value. #animtree emits the current tree’s integer registry index.

Animation-tree names are canonicalized like script filenames. The loader forms animtrees/<name>.atr and resolves referenced animations while the script set is being loaded.

Developer-only code

/# and #/ delimit code controlled by the developer-script setting. They may surround complete top-level function definitions or statement regions:

/#
debug_dump()
{
    println("debug");
}
#/

main()
{
    /# println("entered main"); #/
}

When developer scripts are disabled, these regions are removed during compilation. When enabled, their bytecode is retained as developer code. print, println, and assert are the three developer-only global built-ins in the stock game registry. Outside an explicit developer region, they may be used only as call statements; the compiler marks and conditionally retains that statement as developer code. Inside /# ... #/, the ordinary call-expression rules apply.

wait, waittill, and waittillmatch are not allowed directly inside a developer-only statement region. The stock diagnostic recommends calling a waiting function as a thread. Developer delimiters also cannot directly wrap a case, break, or continue in a way which crosses the compiler’s control-flow boundary, and nested developer function compilation is rejected.

Errors and lifetime

The compiler reports syntax errors, duplicate or missing functions, illegal control-flow placement, invalid literal case values, unresolved scripts and animations, and operand-stack overflow with a source position.

The VM reports dynamic type errors at the executing opcode. Important error classes include:

  • using a non-object as a field or method receiver;

  • indexing a value with the wrong type or outside its valid domain;

  • applying an operator to unsupported or unmatching types;

  • calling a non-function value through [[...]];

  • too many function arguments or nested calls;

  • invalid wait values and invalid event-name types;

  • invoking a built-in with the wrong number or type of arguments.

Values are reference-counted. Strings, vectors, objects, arrays, threads, and archived stacks remain alive while referenced. An entity or thread object can transition to a dead object subtype; later field or method use then fails the object/type checks rather than reviving it.

Compact syntax reference

The following EBNF is a readable summary of the recovered grammar. It omits the precedence expansion already given in the operator table. identifier and script-path are scanner tokens; reference covers local, field, and indexed lvalues accepted by the compiler.

script          = { using-tree | function | developer-function } ;
using-tree      = "#using_animtree" "(" string-literal ")" ";" ;
function        = identifier "(" [ identifiers ] ")" block ;
identifiers     = identifier { "," identifier } ;
developer-function = "/#" function "#/" ;

block           = "{" { statement } "}" ;
statement       = ";"
                | block
                | developer-block
                | reference "=" expression ";"
                | reference compound-assign expression ";"
                | reference ( "++" | "--" ) ";"
                | call ";"
                | "return" [ expression ] ";"
                | "wait" expression ";"
                | "if" "(" expression ")" statement
                      [ "else" statement ]
                | "while" "(" expression ")" statement
                | "do" statement "while" "(" expression ")" ";"
                | "for" "(" [ for-item ] ";" [ expression ] ";"
                      [ for-item ] ")" statement
                | "switch" "(" expression ")" block
                | "case" ( integer-literal | string-literal ) ":"
                | "default" ":"
                | "break" ";"
                | "continue" ";"
                | object "waittill" "(" expression
                      { "," identifier } ")" ";"
                | object "waittillmatch" "(" expression
                      { "," expression } ")" ";"
                | object "notify" "(" expression
                      { "," expression } ")" ";"
                | object "endon" "(" expression ")" ";" ;

developer-block = "/#" { statement } "#/" ;
for-item        = reference "=" expression
                | reference compound-assign expression
                | reference ( "++" | "--" )
                | call ;
call            = [ object ] [ "thread" ] callable
                      "(" [ expressions ] ")" ;
callable        = identifier
                | script-path "::" identifier
                | "[[" expression "]]" ;
function-value  = "::" identifier
                | script-path "::" identifier ;
expressions     = expression { "," expression } ;
reference       = identifier
                | object "." identifier
                | indexable "[" expression "]" ;
compound-assign = "|=" | "^=" | "&=" | "<<=" | ">>="
                | "+=" | "-=" | "*=" | "/=" | "%=" ;

Stock callable surface

The parser treats game functions and methods as normal call syntax. Their names are resolved by the stock game module during compilation. See United Offensive stock script API registry for the complete recovered global-function, method, and field registries and their lookup order.

Implementation basis

The specification above is derived from these maintained stock-source areas:

Area

Source modules

Scanner and parser

script_yy_runtime.c, script_yy_tokens.c, and the generated POSIX/Windows scanner and parser tables/builders.

AST and compilation

script_compile_types.h, script_compile_expr.c, script_compile_statements.c, script_compile_load.c, and script_code_emit.c.

Runtime values and containers

script_vm.cpp, script_value.c, script_variable.c, and script_array.c.

Threads and events

script_thread.c, script_notify.c, and the wait/event VM opcodes in script_vm.cpp.

Animation trees

script_anim_runtime.c and the animation compiler/runtime paths.

Game API

The stock registries under src/server/game summarized in United Offensive stock script API registry.