ACS

From DoomWiki.org

Doom level format

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.

The ZDoom source port added ACS support then extended the language, including new operations, functions, and a more advanced bytecode format.

Language characteristics[edit]

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. 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.

// This is a comment
int AvailableToAllScripts;

SCRIPT 1 OPEN {
    if (AvailableToAllScripts == 101){
        restart;
    }
}

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[edit]

Once the scripts have been compiled and saved to the BEHAVIOR lumps of the levels, the map designer uses the ACS_Execute action special (#80) to execute the contents. The action can be applied to any linedef, such as 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 number of the script and the map the script is intended to run on. If the target script belongs to a different level than where it was triggered, the script is executed as soon as the map is entered.

#80 Action parameters
Script number
Map number
Argument 1
Argument 2
Argument 3

The 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 they are 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, they do allow the mapper to create imaginative puzzles around items, especially if the keys are replaced with other artifacts. UsePuzzleItem (#129) is similar to the previous action, as it prevents a script from being triggered unless the player possesses the right one of the 17 puzzle items.

Script arguments[edit]

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 hardcoding tag numbers or other values in different scripts, the mapper can include the numbers in the ACS_Execute special. Thus, they are able to set different target sectors, speeds, heights or such for distinct events while they are based on a single script.

The numbers, when provided in this manner, are referred to as arguments. ACS_Execute provides support for three arguments whereas the locked version of the action special only supports two. Below is an example script and an overhead view of the map where the script is meant to take place.

SCRIPT 2 (int arg0, int arg1) 
// The definitions tell the script to use argument data from the linedef it was triggered
{
  Door_Open(arg0,16);
  tagwait(arg0);
  changefloor(arg1,"F_014");
  Floor_RaiseByValue(arg1,8,48);
}
"MAP01" of a PWAD

In this script, tag numbers are expected as the arguments, and a sequence of events is made to occur on target sectors identified by them. When 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, the floor texture is set to change on another sector, which is then commanded to rise by 48 map units with a speed of eight. The last two commands simulate a bridge rising from fluid, an effect that is fairly common in Doom engine game worlds.

The white numbers in the adjacent picture represent the tag numbers of the sectors. The sectors marked with numbers 1–3 have been made to behave like doors while the sectors with tag numbers 4–6 have been made to work as bridges. The three sections are accessed by pressing a linedef in front of a door. To execute the contents of the script above, 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 one of the linedefs, the variables arg0 and arg1 assume the values of Argument 1 and 2 respectively. 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 (delay, tagwait, polywait, scriptwait) in a module script prevents other linedefs from starting it again before the 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 is largely the same for each window, 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, storing information to be used as arguments such as the tag of the floor that needs to be lowered in order to remove the blocking feature of the wall, and the number of the map thing that is used to spawn shards of glass.

Randomization[edit]

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[edit]

By first setting up a script variable and assigning it a random value (here done with the random function), actions executed only when a particular random number is chosen can be created using the if conditional statement. More such statements can be used to create alternative random cases. Below is an example of a script where randomization is involved. Once the sequence is activated, one of the defined ambient sounds gets played.

SCRIPT 3 (void)
{
  int Select; // This is a script variable

  Select = random(1,3);
  if(Select == 1)
  {
    ambientsound("Ambient2",127); // Plays a cricket chirping sound (with full volume)
  }
  else if(Select == 2)
  {
    ambientsound("Ambient5",127); // Frog croaking
  }
  else if(Select == 3)
  {
    ambientsound("Ambient7",127); // Bird singing
  }
}

The maps in Hexen contain diverse applications of randomization. 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. The sword acts like an arrow showing the column plate that works as a teleporter back to the hub map. By stepping onto any of the other three plates, the ceiling crashes down on the player, dealing severe damage. The puzzle is an example of a script that, during a given execution, is set to randomly choose a single case among many to execute only those select actions. The randomly drawn number subsequently determines the behavior of the column plates that have a different script assigned to them.

Setting a random order for multiple events[edit]

Randomization can also be utilized in a script to exhaust a collection of events in a random order during different phases of the map. With the aid of map variables, the script is able to remember events already drawn. If the function draws a number corresponding to an event that was already executed, a test can be used to catch this case and perform a redraw (with the restart statement, which restarts the script as if it were executed again). When a hitherto unused value is finally found, the test is passed and the actions corresponding to the value are run. A way to implement such a script is demonstrated below.

int Lock;
int Lock2;
int Lock3; // These three are map variables

SCRIPT 3 (void)
{
  int Select;

  Select = random(1,3);
  if((Lock == Select) || (Lock2 == Select) || (Lock3 == Select)) // || means OR
  {
    restart;
  }
  if(Select == 1)
  {
    ambientsound("Ambient2",127);
    Lock = 1;
  }
  else if(Select == 2)
  {
    ambientsound("Ambient5",127);
    Lock2 = 2;
  }
  else if(Select == 3)
  {
    ambientsound("Ambient7",127);
    Lock3 = 3;
  }
}

This form of randomization has a significant role in the Hypostyle where the central hall has locked doors in all cardinal directions. 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 it, another door in the hall is chosen to open similarly, revealing a new area and another switch. Once the switch in the fourth area has been triggered, 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 compared to the above example (it uses a do while loop 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[edit]

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 executed again, it continues from where the previous run left off. For instance, if an event requires three separate toggles to be pressed, the following script can be used.

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 suspend statement over and over again. A map variable can be employed to remember past switch activations, such as in the following example for a map with 12 switches.

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 to lower a staircase to a clock gear. The map variable technique has been utilized in the scripts that control the series.

Extended ACS[edit]

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.

See also[edit]

External links[edit]

Source[edit]


ACS
Fundamentals:
Utilities:
Hexen usage:
Other: