Note: We'd like to thank Quasar for allowing us to modify the docs of the Eternity Engine to reflect Legacy FraggleScript implementation.
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:
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:
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.
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:
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.
One way in which scripts can accomplish things is to interact with variables.
Variables can be of three natures, explained as follows:
Built-In — These variables are always present and are defined by the
FraggleScript runtime. They can be accessed by any script. Their value typically
defaults at the beginning of each level.
Built-in variables, current to Legacy 1.40, include:
Global — These variables are defined outside of any script, either in a header
file or in the [scripts] section of the MapInfo lump. Any scripts in the current
level can access these types of variables. If global variables are declared with
the const keyword, they are constants, and if they are
declared with the hub keyword, then the current list
of hub variables will be searched by name for a match when the declaration is encountered.
Hub global variables persist between levels and can be accessed and modified by
scripts in any level until the current episode ends.
Examples:
Note that const variables adapt to the default type for their provided literal, while hub global variables require explicit typing.
Local — These variables are declared inside a script. They can only be accessed
within the script itself, and are destroyed when the script completes execution.
Example:
Note that the print function in this example will print the string "1" and not "0" because local variables always take precedence over any built-in or global variables. This is an important distinction to remember.
Variable names may be of arbitrary length, but should not be named with any FraggleScript reserved word.
FraggleScript has four primary data types as follows:
int -
32-bit signed integer. Only decimal integer literals are accepted.
Example:
fixed (also float) -
a 32-bit fixed-point number, somewhat similar to floating-point except that the
decimal place is fixed at 16 bits so that the word is evenly divided into
integer and decimal parts. fixed numbers must be specified with a decimal point,
otherwise the literal will be interpreted as an integer. fixed values are used
for high precision interaction with the game world.
Example:
string - a string of ASCII characters. FraggleScript strings are limited in length to 256 characters.
The following escape sequences are supported:
\n - line break
\\ - a literal \ character
\" - a literal " character
\? - a literal ? character
\a - bell character - causes the console to play a sound
\t - tab
\0 - write white text
Strings must be delimited by quotation marks as follows:
mobj - an opaque reference to a Doom mapthing
(i.e. monster, lamp, fireball, player). The values of these references must either
be obtained from object spawning functions, or can be specified by use of
integer literals, in which case the mobj reference will point to the mapthing
numbered by the map editor with that number.
Examples:
Note that using map editor numbers for things has the distinct disadvantage that when the map is edited, the things will be automatically renumbered if any are deleted. It is suggested that the latter form of mobj reference assignment be avoided unless necessary.
mobj references are very powerful and allow a large number of effects and fine control not available in other languages such as ACS.
Also note that although integer literals can be used to assign mobj reference values, mobj and int are not interchangeable, and statements such as
are not, in general, meaningful.
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.
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: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: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:
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: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: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: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: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 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.
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: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: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():This is additionally a fine example of how to do ambient sounds with FraggleScript.
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: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: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.
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: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: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 are simply the different ways in which scripts can be started.
Currently supported activation models include the following:
startscript function
Used to start scripts from within FraggleScript code. There are two possible uses of this function. If used from the outer context, the script will be started automatically at the beginning of the level, and player 0 will be used as the trigger object. If used from inside another script, the current script's trigger object will propagate to the new script.
StartScript codepointer
Used to start scripts from mapthing frames, this method relies on and interacts with DeHackEd editing. To use this method, use a BEX codepointer block to place the StartScript pointer into a thing's frame, and then set the frame's "Unknown 1" field to the script number to call. The thing whose frame called the codepointer becomes the trigger object. This allows the user to program their own custom codepointer effects in FraggleScript.
linedef activation
A host of new linedef types are provided to allow activation of scripts from within levels:
code | trigger | description |
---|---|---|
272 | WR | Start script with tag number |
273 | WR | Start script, 1-way trigger |
274 | W1 | Start script with tag number |
275 | W1 | Start script, 1-way trigger |
276 | SR | Start script with tag number |
277 | S1 | Start script with tag number |
278 | GR | Start script with tag number |
279 | G1 | Start script with tag number |
To use these lines, simply give them the appropriate type and set the linedef's tag number to the script number you want to be called. The trigger object will be set to the object that activated the line.
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)".
This gives FraggleScript a comfortable object-oriented appearance.
This is a short alphabetical list of keywords in FraggleScript. Variables cannot be named these words because they are reserved.
const | if |
else | int |
elseif | mobj |
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.