Mission scripts

The goal of the mission script system in DeathDawn is to provide a simple method for developers and modders to add or modify missions; a method that doesn't require the recompilation of the source code, won't invalidate savegames each time a change is made, and won't cause the game to crash if something goes wrong. Taking hints from various other games, the most logical choice of action was to implement a scripting language.

Choice of language

Although many open-source scripting languages are in existence, the decision to implement my own language was made based around the following points:

Language properties

The mission scripting language is an imperative language with a simple syntax and the following features:

Data types

TypePrefix characterDescription
Integeri32-bit signed integer
Fixed pointf16.16-bit fixed point number
Vectorv3D vector, consisting of 3 16.16-bit fixed point numbers. Components are referenced by name (.x, .y, .z)
StringsNull-terminated ASCII string
Label.Code label. Cannot be read or written by the script; can only be used in statements to jump to lines of code.
PedpPed reference. Takes the integer value 0 to indicate a null reference.
CarcCar reference. Takes the integer value 0 to indicate a null reference.
ObjectoObject reference. Takes the integer value 0 to indicate a null reference.
EntityeEntity reference - can refer to either a ped, car, or object. Takes the integer value 0 to indicate a null reference.
Gang listgA set of bitflags, representing none, some, or all of the 32 gangs.

Bytecode execution

Before describing the parser, it's important to describe the language that the parser compiles to.

Scripts are compiled into an array of token structures; each token has a type and a data value. Types are:

TypeDescriptionValue
TOKEN_WORDReserved word; typically something which can be executed.data.w contains the ID of the reserved word
TOKEN_VARVariable. Cannot be executed.data.v points to the appropriate mvar (mission variable) object
TOKEN_LINELine number. Execution merely causes the 'current line number' variable (sline) to be updated.data.w - line number

Execution begins with the run_script2() function, which identifies the correct script_ip value to begin execution from, and processes each token in turn starting at that location. For each executable token, the .exec entry must be defined in the token definition. This is the name of the C function to call. That C function is free to manipulate the state of the script execution engine, typically by reading variables from the token stream (via script_getvarpair()), adjusting the IP value to branch to a new section of script, or setting the script_running flag to false to stop execution.

Gosubs

The scripting engine supports subroutines via the GOSUB and RETURN script commands. A simple 128-entry stack of IP values is used for this.

Variable stack

Independent of the gosub stack is the variable stack. This is implemented as part of the mission variable handler (mvar code). A list of variables can be pushed onto the stack using the PUSH command. These can later be restored to previous values using the POP command (which removes an entire level of the stack, restoring the variables pushed to their previous values). The depth of the stack is only limited by how much memory is available. The stack is flushed before and after each script execution, to make sure no lingering entries are left behind.

Parser structure

The parser has been developed without the use of yacc, bison, or similar; this has kept the code and specification simple, but has resulted in a somewhat muddled implementation. The core of the parser is the readscriptline() function; its operation is to read a single line of input from the input file, split it into its three components (label, operation, comment) and add the appropriate tokens to the token stream.

Input buffer

There are several functions to examine and read from the input line buffer:

Constant values

As stated above, constant values are converted to variables by the parser. This includes all numbers, strings, vectors, etc. A semicolon is used as part of the variable name, to ensure that such a name can never be used as part of legal script. The rest of the variable name consists of the value of the variable, in a string-safe form.

Although constant values should be constant, there are not many safeguards in place to ensure that entrepeneuring programmers cannot change their value. However, constant variables are not saved in savegames, so any changes will merely be temporary.

.properites

Some properties of vectors, peds, cars, objects and entities can be read and written using .property and PROPERTY= tokens. PROPERTY= tokens are hidden, in the sense that the presence of the '=' character prevents them from being read directly from a script file. PROPERTY= tokens can therefore only be inserted into the token stream by the lookahead code in getword(). .property tokens can be read from the script file, but only in certain circumstances (to avoid them getting confused with labels). The basic rule (as used by varname()) is that if a '.' appears at the start of the input line, or has whitespace preceeding it, it must be the start of a label; else a (.property) reserved word. This check isn't used by getword(), however.

Prefix notation

The token parser converts the input line into prefix notation, to simplify the execution of the bytecode. However due to the way arrays and argument lists are handled, it does introduce some extra complexity to the algorithm. addtemptoken() and dotemptokenadd() are the two functions to look at.

The expression rewriting inherent in converting to prefix notation also involves using temporary variables to store the results of subexpressions. For each line, unique temporary variable names are used; these names are generated using the variable type character, two semicolons (to distinguish from constant variables), and a sequence counter.

Required/special symbols

There are a small set of labels/variables that are used by the game engine to interface with the mission script:

TODO:
DOCS: UNKNOWN: Are there any more bits that need explaining? Go into more detail about the prefix conversion, converting array indicies to temporary variables yet retaining ability to write to arrays using command-style tokens (I think!), etc.
DOCS: UNKNOWN: How the comma auto-insert/removal works, other bits like argument lists, etc.
DOCS: MED: How the game config script should work
CODE: UNKNOWN: Add more script commands for ped/car/obj inspection/manipulation. Getting which car a ped is in, .anim property for obj/ped?, .faction property to get which faction(s) it's in, etc. Also need functions to move peds into/out of cars.
CODE: UNKNOWN: Fix negative numbers vs. sub/uminus
CODE: LONG: Add more memory checking - out of memory, asserts, etc.
CODE: QUICK: Check that label arrays work
CODE: MED: Add INCLUDE support to mission scripts. Would be good way of sharing common code between scripts. Can easily implement it in a similar way to simpscript (perhaps using same code, if simpscript was enhanced to detect comments properly). Can upgade TOKEN_LINE to contain line number in bottom 24 bits, file number in top 8 - and add file name array/buffer thing to track the different names.
MUST-CODE: MED: Make sure all code that calls script_error() shuts down game properly if the function returns.
CODE: MED: Check whether parser is robust enough for script statements which take arguments (and return a value) can be used as functions (which take arguments and return a value)
MUST-DOCS: UNKNOWN: Script parser caveat: ADDCOLOUR X*Y B ... doesn't work properly, but ADDCOLOUR X*Y,B or ADDCOLOUR (X*Y) B does. Also, need to check ADDCOLOUR(X*Y,B), etc. And document findings! (design docs + editor docs)
???
Profit!