United Offensive scripting language¶
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 |
|---|---|
|
Absence of a value. Unset locals, missing array entries, omitted parameters, and functions without a value result use this type. |
|
Signed 32-bit integer payload. |
|
IEEE-754 binary32 value. |
|
Interned ordinary string. |
|
Interned localized-message string written with |
|
Three binary32 components. |
|
Reference to an entity, struct, array, thread object, or another field-bearing VM object. |
|
Reference to compiled script code. |
|
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 value. |
|
Integer zero / integer one. |
|
Ordinary string. |
|
Localized string. |
|
Vector; each of the three values is cast to float at runtime. |
|
New empty array. |
|
Animation reference resolved through the file’s active animtree. |
|
Integer handle of the file’s active animtree. |
|
Same-file function value. |
|
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 requires0 <= 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 |
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 |
|
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 |
|---|---|
|
Int, float, or ordinary string. The result is integer 0 or 1. |
|
Int, float, or ordinary string. Float conversion truncates according to the stock target conversion; strings use the stock decimal parser. |
|
Float, int, or ordinary string. The result is binary32. |
|
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 typesruntime 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 |
|
float |
|
vector |
Vector |
ordinary 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
undefinedvalues 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 |
|---|---|
|
Call a named same-file script function or a stock built-in function. |
|
Call a named function in another script. |
|
Method-context call; |
|
Call through a function value. |
|
Call through a function value with explicit |
|
Execute the named function as a new child thread using inherited
|
|
Execute a child thread with explicit |
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 |
|
AST and compilation |
|
Runtime values and containers |
|
Threads and events |
|
Animation trees |
|
Game API |
The stock registries under |