Note: We'd like to thank Quasar for allowing us to modify the docs of the Eternity Engine to reflect Legacy FraggleScript implementation.

FraggleScript Basics

Last updated 2016-09-21

Getting Started

FraggleScript (FS) is a simple C-like scripting language that can be used to add complex functionality into a Doom map. It was originally developed by Simon "Fraggle" Howard for his Doom source port SMMU, and is currently used by several Doom engines including Doom Legacy. FS has evolved into several dialects since its creation, so perfect FS compatibility between different engines unfortunately cannot be expected.

To get started using FraggleScript in your Doom levels, you'll need to fully understand most aspects of Doom level editing and WAD file manipulation. If you haven't mastered this basic stuff, it would probably be wise to read the other editing docs first, and maybe check out the Doomworld tutorials section and to look up a few FAQs. This manual assumes you understand basic Doom editing.

FS scripts reside inside the MapInfo header, in the [scripts] block. The MapInfo header itself is just an ASCII text file which is stored in the map separator lump, which has the same name as the map (e.g. E1M4, MAP21...) and is normally empty. Each map can have its own MapInfo, and thus its own scripts. There are also ways to share scripts between maps, more of which later.

When you first want to create scripts, you should create a blank file using a suitable text editor, named something appropriate like "map01.fs" The .fs extension is not required, but its useful for figuring out what and where your files are later on. When you have the file, you need to place a block header in it like this:

[scripts]

This tells the game that your MapInfo is declaring a section for scripts. If you want to define other MapInfo blocks, you can put them before or after this section.
Example:

[level info] levelname = The Palace of w00t creator = Quasar [scripts]

After the header is in place you can begin defining scripts and variables. When you're done, simply insert the file you just created into the appropriate map separator lump using a WAD editor.

Defining Scripts

Scripts are the basic subprogram unit of FraggleScript, similar to the functions of C and the procedures of Pascal. FraggleScript scripts do not take any explicit parameters, however, and cannot return values, which is quite different from most languages.
Levels can currently have up to 256 scripts, numbered from 0 to 255 (future expansion of this number is possible to allow for persistent scripts). Scripts exist only within the level to which they belong, and for the most part only affect that level, with the exception of hub variables.
To declare a script, follow the syntax of the following example:

[scripts] script 0 { }

The script keyword denotes that the script definition is starting, and the number following it is the number for this script. Script numbers should always be unique, one per defined script.

The script above is valid, but it is not very interesting because it does nothing, and a script alone cannot run without first being called. Scripts may be invoked in several manners, which is covered in the Script Activation Models section.

Variables and Data Types

One way in which scripts can accomplish things is to interact with variables.

Variables can be of three natures, explained as follows:

Type Coercions

FraggleScript is a weakly typed language, and as such, coercions are made freely between all data types.

These coercions follow the rules below:

Conversion to int from:

Conversion to fixed from:

Conversion to string from:

Conversion to mobj from:

Coercion is an automatic process that takes place when a variable is assigned the value of a variable of a different type, when values are passed to functions that do not match the specified parameter types, and when operands of multiple types are used with some operators. Some functions may perform more strict type checking at their own volition, so beware that script errors may occur if meaningless values are passed to some functions.

Calling Functions

FraggleScript offers an extensible host of built-in functions that are implemented in native code. They are the primary means to manipulate the game and cause things to happen. The FraggleScript Function Reference is a definitive guide to all functions supported by the Legacy dialect; this document will provide some basic examples of function use.

Most functions accept a certain number of parameters and expect them to represent values of specific meaning, such as an integer representing a sector tag, or an mobj reference to a mapthing to affect.

The function reference lists the parameters that each function expects, but there are some important things to take note of. As mentioned in the previous section, type coercions can occur when functions are passed parameters of other types.

An excellent example is the following:
script 0 { startscript("1"); }

The startscript function expects an integer value corresponding to the number of a script to run. Here it has been passed a string, "1". Strings will be converted to the integer they represent, if possible, so this string is automatically coerced into the integer value 1, and the script 1 will be started.

An example of a coercion that is NOT meaningful in the intended manner would be the following:
script 0 { mobj halif = 0; startscript(halif); }

Fragglescript mobj references can be assigned using integer literals, but the rules for coercion from mobj to int state that -1 is always returned for an mobj value (this is because there is *not* a one-to-one mapping between mobj references, which can include objects spawned after map startup which do not have a number, and integers).

This statement has the effect of calling startscript with -1, and since -1 is not in the domain of startscript, a script error occurs.

Effect of mobj reference, an Error:
startscript(-1);

Parameter coercions of this type should be avoided for purposes of clarity and maintainability of your code. When coercions are convenient or necessary, be certain that the value you obtain through coercion will always be meaningful.

Note that some functions, like print, can take a variable number of parameters. These types of functions generally treat all their parameters in a like manner, and can accept up to 128 of them. See the reference to get a better idea about these types of functions and what they do.

Functions may return values, and in fact, most useful functions do. To capture the return value of a function, you simply assign it to a variable.

Example of a return value:
script 0 { fixed dist; dist = pointtodist(0, 0, 1, 1); }

This places the fixed-point distance between the points (0,0) and (1,1) into the fixed variable dist. It is *not* necessary to capture the return value of a function simply because it has one.

Example of ignoring a return value:
script 0 { pointtodist(0, 0, 1, 1); }

A statement ignoring the return value of a function like pointtodist is valid, and is normal when using functions with side effects, like changing the program state, or program environment. But since pointtodist has no side effects, returning a value is all it does, the call in this particular example is suspiciously useless. Likewise, it is possible to use function return values without explicitly capturing them in variables, such as using them directly as a parameter to another function, or testing them directly.

Example directly using the result of one function as a parameter:
script 0 { print(spawn(IMP, 0, 0, 0)); }

This causes a new imp to be spawned at (0,0,0), a side effect. Since spawn additionally returns a reference to the new object, the mobj reference returned by spawn becomes the parameter to the print function, and the resulting output to the console is "map object". The value returned by spawn "disappears" after the print function has executed and cannot be retrieved or otherwise used. This is useful when you don't need to save the return value of a function beyond using it as a parameter.

If a function is listed as being of return type void, this means that it does not return a meaningful value and that it operates by side effect only, like a procedure in some languages. However, unlike in C, void functions in FraggleScript, will return the integer value 0, rather than causing an error when used in assignments. In practice, it is best not to assign variables the null return value from a void function even though it is allowed, since this is confusing to see in code and useless anyways.

Note that it is possible to make limited function calls outside of any script, in the surrounding program environment, which in FraggleScript is referred to as the levelscript. Function calls placed in the levelscript will be executed once (and only once) at the beginning of the level. This is commonly used to start scripts to perform certain actions.

Levelscript example:
[scripts] script 0 { message("Welcome to the Palace of w00t, foolish mortals!"); } startscript(0);

In this example, all players would see this message at the beginning of the level. It is not required, but is good style, to put any levelscript function calls after all script definitions.

Flow Control Structures

Flow control structures allow your code to make decisions and repeat actions over and over. There are several basic control structures, and each are covered here in full.

while

The while-loop is the basic loop control structure. It will continually loop through its statements until its condition evaluates to 0, or false.

Loop basic syntax:
while(<condition>) { <statements> }

Unlike in C, the braces are required to surround the block statement of a while-loop, even if it only contains one statement.

Loop requires braces:
script 0 { int i = 0; while(i < 10) { print(i, "\n"); i = i + 1; } }

This code would print the numbers 0 through 9 to the console.

The continue() and break() functions are capable of modifying the behavior of the while-loop. A continue() causes the loop to return to the beginning and run the next iteration. A break() causes the loop to exit completely, returning control to the surrounding script.

A while-loop can run forever if its condition is never false, but if you write a loop like this, be sure to call one of the wait functions inside the body of the loop, or else the game will wait on the script to finish forever, effectively forcing you to reboot!

Forever loop needs a wait():
script 0 { while(1) { if(rnd() < 24) { ambientsound("dsbells"); } wait(20); } }

This is additionally a fine example of how to do ambient sounds with FraggleScript.

for

The for-loop is a more sophisticated loop structure that takes three parameters. Note that unlike C, the FraggleScript for-loop parameters are separated by commas, not by semicolons, and all 3 are required to be present.

Loop syntax:
for(<initialization>, <condition>, <iteration>) { <statements> }

All three statments may be of any valid form, although typically the initialization statement sets a variable to an initial value, the condition statement checks that that variable is within a certain bound, and the iteration statement increments the variable by a certain amount.

Braces are again required. The continue() and break() statements work similar as they do in a while-loop, except that a continue() in a for-loop will check the for-loop condition and perform the for-loop iteration.

Operative example of a for-loop:
script 0 { int i; for(i=0, i<10, i=i+1) { print(i, "\n"); } }

This for-loop is equivalent to the while-loop example above. In general, while-loops and for-loops are logically equivalent, the for-loop simply provides a cleaner way to specify the exact loop behavior. The for-loop is rather complex statement but is very useful, so if this explanation is insufficient, any decent reference on the C language should have more examples of for-loop usages that might be applicable.

if, elseif, else

The if-stmt tests its condition, and if the condition evaluates to any non-zero value, or true, the statements inside its body are executed. Braces are not required for a simple if-stmt (unlike the while-loop and for-loop), which has no clauses. Braces are required when the if-stmt body contains more than one statement, or when there is an elseif-clause or else-clause attached to the if-stmt.

Basic syntax of the if-stmt:
if(<condition>) <statement> if(<condition>) { <statements> }

Examples of if-stmt:
if(i > 10) return();
if(i) { print(i, "\n"); return(); }

The elseif-clause and else-clause are ancilliary clauses of an if-stmt that execute when their attached if-stmt was not true. They must immediately follow their attached if-stmt, else they will be illegal syntax. The elseif-clause tests an additional if-stmt condition, and when true, executes its code section. If it is false, control passes to the next elseif-clause or else-clause, if one exists. An if-stmt can have any number of elseif-clause attached. There can only be one else-clause attached to an if-stmt, and it must be last. The else-clause executes its code section when all of the attached if-stmt and elseif-stmt test conditions evaluated to false. When the if-stmt evaluates to true, any attached elseif-clause or else-clause will not be executed, nor will their test conditions be evalutated.

Complex if-stmt example:
script 0 { int i = rnd()%10; if(i == 1) { spawn(IMP, 0, 0, 0); } elseif(i <= 5) { spawn(DEMON, 0, 0, 0); } else { spawn(BARON, 0, 0, 0); } }

This example, as you should expect, spawns one enemy at (0,0,0), its type depending on what random value i was assigned.

Note that elseif and else are new to the Eternity and Doom Legacy dialects of FraggleScript and that they are not currently supported by SMMU.

Script Activation Models

Script activation models are simply the different ways in which scripts can be started.

Currently supported activation models include the following:

Operators and Operator Precedence

The Legacy dialect of FraggleScript supports the following operators, in order of precedence from greatest to least. All operators except = are evaluated from left to right.

.

Coercions: none
The dot-structure operator - can be used to beautify function calls by moving the first argument outside the parameter list. A function call "xx.fn(2)" is the same as "fn(xx,2)".

Object-oriented function call example:
script 0 { mobj halif = spawn(DWARF, 0, 0, 0); halif.kill(); }

This gives FraggleScript a comfortable object-oriented appearance.

--
Coercions: string -> int, mobj -> int
The decrement operator - lowers the value of the operand by 1. As a prefix operator (appears before the operand), the decrement is performed before the value can be used. As a postfix operator (appears after the operand), the variable's current value is propagated, and then the decrement occurs.
++
Coercions: string -> int, mobj -> int
The increment operator - increases the variable's value by 1. As a prefix operator (appears before the operand), the increment is performed before the value can be used. As a postfix operator (appears after the operand), the variable's current value is propagated, and then the increment occurs.
!
Coercions: all -> int
The logical invert operator. Returns 1 if the operand has an integer value of 0, and 0 otherwise.
~
Coercions: all -> int
The binary invert operator. Reverses all bits in the operand's integer value (i.e. 10101 becomes 01010)
%
Coercions: (all*all) -> (int*int)
The modulus operator. Returns the remainder when its first operand is divided by its second.
/
Coercions: (string*all) -> (int*int), (mobj*all) -> (int*int), (fixed*all) -> (fixed*fixed)
The division operator. Returns the quotient of the first operand divided by the second. Note that if either operand is of type fixed, the other operand is coerced to type fixed.
*
Coercions: (string*all) -> (int*int), (mobj*all) -> (int*int), (fixed*all) -> (fixed*fixed)
The multiplication operator. Returns the product of its operands. Note that if either operand is of type fixed, the other operand is coerced to type fixed.
-
Coercions: (string*all) -> (int*int), (mobj*all) -> (int*int), (fixed*all) -> (fixed*fixed)
The subtraction and unary minus operators. Returns the difference of its operands. FraggleScript treats both subtraction and unary minus signs the same; an expression such as -1 is evaluated as if it were 0-1. If either operand is of type fixed, the other operand is coerced to type fixed.
+
Coercions: (string*all) -> (string*string), (mobj*all) -> (int*int), (fixed*all) -> (fixed*fixed)
The addition operator. Returns the sum of its operands. If either operand is of type fixed, the other operand will be coerced to type fixed.
>=
Coercions: (string*all) -> (int*int), (mobj*all) -> (int*int), (fixed*all) -> (fixed*fixed)
The logical greater than or equal operator. Returns 1 if its first operand is greater than or equal to its second, and 0 otherwise.
<=
Coercions: (string*all) -> (int*int), (mobj*all) -> (int*int), (fixed*all) -> (fixed*fixed)
The logical less than or equal operator. Returns 1 if its first operand is less than or equal to its second, and 0 otherwise.
>
Coercions: (string*all) -> (int*int), (mobj*all) -> (int*int), (fixed*all) -> (fixed*fixed)
The logical greather than operator. Returns 1 if its first operand is greater than its second, and 0 otherwise.
<
Coercions: (string*all) -> (int*int), (mobj*all) -> (int*int), (fixed*all) -> (fixed*fixed)
The logical less than operator. Returns 1 if its first operand is less than its second, and 0 otherwise.
!=
Coercions: (mobj*all) -> (int*int), (fixed*all) -> (fixed*fixed)
The logical nonequality operator. For strings, this operator tests against string equality, and for numeric types, it tests against numeric equality. If either operand is of type fixed, the other operand will be coerced to type fixed.
==
Coercions: (mobj*all) -> (int*int), (fixed*all) -> (fixed*fixed)
The logical equality operator. For strings, this operator tests for string equality, and for numeric types, it tests for numeric equality. If either operand is of type fixed, the other operand will be coerced to type fixed.
&
Coercions: (all*all) -> (int*int)
The binary and operator. This operator sets all bits in its result that are set in both of its operands (i.e. 111010 & 100110 = 100010)
|
Coercions: (all*all) -> (int*int)
The binary or operator. This operator sets all bits in its result that are set in either of its operands (i.e. 111010 | 100110 = 111110)
&&
Coercions: (all*all) -> (int*int)
The logical and operator. This operator returns 1 if both its operands evaluate to 1, and 0 otherwise. If the first operand evaluates to 0, this operator will short-circuit without testing the second operand.
||
Coercions: (all*all) -> (int*int)
The logical or operator. This operator returns 1 if either of its operands evaluate to 1, and 0 otherwise. If the first operand evaluates to 1, this operator will short-circuit without testing the second operand.
=
Coercions: coerces type of expression into type of variable
The assignment operator. This operator sets the variable on its left to the value of the expression on its right. All other evaluations are performed before an assignment takes place.

Keyword List

This is a short alphabetical list of keywords in FraggleScript. Variables cannot be named these words because they are reserved.

const if
else int
elseifmobj
fixed script
float string
for while
hub

Remember that break, continue, return, and goto are defined as special functions, and not as keywords.

While the names of functions are not reserved words in the strictest sense, you should additionally avoid naming variables with the same name as functions since these variables will hide the functions and make them inaccessible. This will cause parse errors if you attempt to use the function after declaring a variable with the same name.