ACS or Action Code Script is a byte code script that was created for Hexen. ACS allows for complex scripted events or interactive environments within a level, making the Doom engine infinitely more open-ended. The primary purpose of the language is to manipulate structures within the level such as sector heights, textures, and Polyobjects. ACS also includes the ability to add, remove, or manipulate actors within the level.

Contents

Language characteristics

Given that ACS is based around byte code, the source language is not necessarily defined. In practice, most code is written for Raven Software's ACS compiler, ACC. The ACS language is syntactically similar to C and thus allows for most of the usual control flow statements (if, do, while, for, until). C-style single line and multiple line comments are supported. ACC supports some basic C preprocessor commands, specifically #define and #include, but they are not parsed in a separate pass and come with some limitations. Another notable limitation of ACC is that it does not support operator short circuiting for && and ||.

ACC recognizes two types of variables: int and str. For the purposes of Hexen these two types are interchangeable as a string is merely a pointer to an index in the string table. Variables can be defined in one of three scopes for variables: world, map, and local. World variables keep their value for all maps in a hub, map variables are specific to the map, and local variables are specific to a script in the map. Normal C scoping rules apply to local variables. All variables are initialized to zero by default and world/map variables can not be initialized to anything else without the help of a script.

Code in ACS is arranged into scripts. These can more or less be seen threads of execution, so their order of execution should be considered undefined. Scripts are identified by an arbitrary number 1 through 999. Which ever numbers are chosen, Hexen is only capable of loading 64 scripts on the active map.

A script can take up to three arguments or have a special execution condition: OPEN, RESPAWN, DEATH, or ENTER. OPEN indicates that the script should be executed during map initialization, RESPAWN is executed by the player when they respawn in multiplayer, DEATH is executed by the player when they die, and ENTER is executed by the player the first time they enter the game.

ACS does not support arrays or custom functions. An infinite loop will cause the game to hang as the ACS VM never gives control back to the game. Some source ports implement an instruction execution cap in order to notify the developer of when this happens without halting the game.

ACS action specials

Once the scripts have been compiled and saved to the BEHAVIOR lumps of the levels, the map designer uses the ACS_Executeaction special (#80) to get the contents implemented. The action can be inserted to any sort of linedef, for instance, a switch or a crossable line. Hexen also allows monster kills and item pickups to trigger action specials so such occurrences can also be set to execute scripts. The ACS special includes numeric information such as the identification figure of the script and the map of which script is intended to run. If the target script belongs to a different level than where it was triggered, the system starts to execute the script as soon as the player transports to the map.

#80 Action parameters

Script number

Map number

Argument 1

Argument 2

Argument 3

ACS_LockedExecute (#83) action allows the mapper to lock the trigger function. Consequently, the player needs to hold the right one of the eleven keys before he is able to start the script. The stock maps in Hexen use this solely for spacebar activations but line walkovers, bumps and projectile hits are also supported by the action special. While some of these methods seem to have little to do with key usage, the features do allow the mapper to create imaginative puzzles around the items, especially if the keys are replaced with other artifacts. UsePuzzleItem (#129) is similar to the previous action, as it prevents a script from triggering before the player possesses the correct object among the 17 puzzle items of the game.

#83 Action parameters

#129 Action parameters

Script number

Item number

Map number

Script number

Argument 1

Argument 1

Argument 2

Argument 2

Key number

Argument 3

Key ID# information

1

Steel

2

Cave

3

Axe

4

Fire

5

Emerald

6

Dungeon

7

Silver

8

Rusty

9

Horn

10

Swamp

11

Castle

Puzzle item ID# information

0

Yorick's Skull

9

Flame Mask

1

Heart of D'Sparil

10

Glaive Seal

2

Ruby Planet

11

Holy Relic

3

Emerald Planet 1 (#9005*)

12

Sigil of the Magus

4

Emerald Planet 2 (#9009*)

13

Clock Gear (Steel)

5

Sapphire Planet 1 (#9006*)

14

Clock Gear (Bronze)

6

Sapphire Planet 2 (#9010*)

15

Clock Gear (Ring of steel)

7

Daemon Codex

16

Clock Gear (Ring of bronze)

8

Liber Oscura

* Thing type numbers are presented for identification.

Script arguments

The ACS action specials contain a feature that allows the mapper to design script modules. In other words, if many game effects on a level share an identical sequence of commands, the map designer does not have to create separate scripts for each one of them. Instead of setting unique tag numbers or other values to multiple scripts, the mapper can insert the numeric data of the commands to the ACS_Execute special itself. Thus, he is able to set different target sectors, speeds, heights or such for distinct events while they are based on a single script.

The figures are stored to parameters called arguments. ACS_Execute provides support for three unique values whereas the locked version of the action special is able to hold two of them. Below is an example script and an overhead view of the map where the script is meant to take place.

In the instance, the script arguments are used to enable varying tag numbers. With them, the mapper is able to make a single sequence occur on several target sectors. As the script is triggered, a door opens with a speed of 16. The run of actions is suspended until the door has reached its destination height (tagwait). Once the door is fully open, a floor texture is set to change on another sector and the sector is yet commanded to raise by 48 map units with a speed of eight. The two previous commands simulate a bridge rising from fluid, an effect that is fairly common in the worlds of the Doom engine games.

The white figures in the adjacent picture represent the tag numbers of the sectors. The sectors marked with numbers 1-3 have been edited to behave like doors whereas the sectors with tag numbers 4-6 have been prepared to work as bridges. The three sections are accessed by pressing a linedef in front of a door. To realize the contents of the script above, the triggers are set in the following way.

Parameter

Left

Middle

Right

Script number

2

2

2

Map number

1

1

1

Argument 1

1

2

3

Argument 2

4

5

6

Argument 3

0

0

0

Once the player presses the linedefs, their argument numbers replace the arg0 and arg1 (Argument 1 and 2 respectively) spaces of the script. As a result, three different doors and bridges can be activated at separate moments and in any desired order by using only a single script. It is notable, however, that any function that causes a pause (void delay, tagwait, polywait, scriptwait) in a module script prevents other linedefs from starting it again before the defined pause has expired. In the example, the tagwait function has the side effect of locking the triggers on other linedefs until the active door has stopped its movement. If such behavior appears to be adverse for gameplay, the mapper can preclude it by either designing the sequence to be devoid of delay functions or by creating a separate script for each case.

Some original maps in Hexen, such as the Winnowing Hall, have stained glass walls. The breaking of such is one of the effects that utilizes script arguments. Since the effect remains consistent, the levels usually have only one script controlling the breaking event. The script specials have been set to each wall to become activated by an impact. The arguments store information such as the tag of the floor that needs to be lowered in order to remove the blocking feature of the wall. Another important piece of information is the number of the map thing that is used to spawn shreds of glass upon breaking.

Randomization

Among notable features, ACS includes a function that allows the map designer to randomize game events. This enabled a new level of replay value to the series of games using the Doom engine as the game content could potentially change between playthroughs. So far, only Heretic's behavior for Firemace placing and the game's implementation of ambient sounds had provided remarkable randomization.

Drawing a single event

By setting up a script variable and assigning it a random value using the proper function (int random;), the mapper can cause a script to run partially. This is completed by dividing the script into cases with the if conditional statement. In the structure, each case practically has an identification number that matches to the values the variable can receive beforehand. Below is an example of a script where randomization is involved. Once the sequence is activated, one of the defined ambient sounds gets played.

The maps in Hexen contain diverse applications of the function. One can be found during the first visit to the Guardian of Ice. When the player has hit the switch at the northern end of the level, four columns rise behind it, and eventually an area resembling a sword becomes illuminated in front of one. The sword acts like an arrow showing the column plate that works as a teleporter back to the hub map. By stepping onto the other three plates, the player causes their ceiling to plummet which produces severe damage to him. The puzzle has an example of a script that, during its sole run, is set to randomly choose a single case among many to execute only its commands. The drawn map variable subsequently controls the behavior of the column plates that have a different script assigned to them.

Setting a random order for multiple events

Randomization can also be utilized in scripts that include parts that are meant to be activated at separate phases of gameplay. Basically, all command cases of a script can be edited to occur in a random order. With the aid of map variables, the mapper is able to set the system to "memorize" which cases have already been run. If the function later draws a figure of which corresponding contents have been executed for once, an additional test can be used to send the script variable for a redraw. When an unused value is finally found, the test is passed and the value's contents are run. A way to implement such a script is demonstrated below.

This form of randomization has a significant role in the Hypostyle where the central hall has locked doors in all cardinal points. Once the player has eliminated a specific ettin in the hall, one of the four doors is randomly selected to open. Each door seals an area that has a switch at the end of it. When the player activates such, another door in the hall is chosen to open similarly, revealing a new area and a toggle. Once the switch in the fourth area has been triggered, yet two more doors open in the hall and the player can enter either one to confront the death wyvern. Although the script that controls the puzzle has a different structure in comparison with the above example (it uses a do while iteration statement in addition to if statements), the common idea is the same and they both result in their events being executed in a random order.

Series of actions

While advancing in Hexen, the player frequently encounters events that require more than just one action to occur. To create such puzzles, the mapper can use the suspend; control statement in a script to interrupt its run of commands. Once the script is called again, the execution continues from where the previous run left. For instance, if the realization of an event requires three separate toggles to be pressed, the following script applies.

SCRIPT 4 (void)
{
suspend;
suspend; // The statements "absorb" the effect of the two first toggles, whichever they are
ambientsound("Chat",127);
printbold(s:"SEQUENCE COMPLETED!");
}

If there is a vast amount of switches, it is not necessary to repeat the keyword over and over again. Given that the puzzle includes twelve toggles, a map variable can be assigned to produce a more convenient structure.

int Count;
SCRIPT 4 (void)
{
Count++; // This increases the variable's value by one each time the script is run (i.e. a new switch is pressed)
if(Count == 12)
{
ambientsound("Chat",127);
printbold(s:"SEQUENCE COMPLETED!");
}
}

Multiple series of switches can be met in the Castle of Grief. In the beginning, the player needs to activate two bull switches to raise a drawbridge over the moat. Then he has to find and press four moon switches outside the walls of the castle to lower a platform in the tower. Finally, he needs to visit the five guard towers and push a skeleton switch in each one of them to lower a staircase to a Clock Gear. The map variable technique has been utilized in the scripts that control the series.

Extended ACS

An extended byte code format known as ACSe (Hexen's ACS format is sometimes called ACS0 based on the fourCC in the header) originates from ZDoom and extends ACS in various ways. Some key additions include support for named scripts, static initialization of map variables, custom functions, arrays, the global scope, and libraries. The global scope works similarly to the world scope, but applies to all maps in a game session, not just those in the current hub. Typically these are used in combination with ACS libraries which allows compiled ACS modules to be reused between maps. The underlying byte code uses the same, although expanded, instruction set so a separate VM is not needed to support both types of byte code.

Support for extended ACS has since been implemented in the Eternity Engine.