Skip to content

ScummC Grammar

Alban Bedel edited this page Apr 5, 2015 · 1 revision

1. Introduction

The ScummC grammar is mostly inspired by C, so it looks like C with a lot of new constructs for the elements needed to create a game. A SCUMM game consists of rooms, with each room containing scripts and the necessary game resources like graphics, sounds, etc.

A ScummC source file consists of global resource declarations (like global variables, actors, etc.) and room blocks. Includes and comments are supported with the same syntax as in C. However, includes with angle brackets ("<inc.h>") aren't supported because scc has no concept of an include directory.

2. Global declarations

Global declarations have one of the following syntax:

TYPE NAME location ;

TYPE and NAME should be obvious. location can optionally be used to specify the address that will be used. Generally, the address is allocated dynamically at link time; specifying it by hand is mostly useful for declaring the system variables used to communicate with the engine.

TYPE is one of:

  • room
  • actor
  • verb
  • class
  • int, word
  • char, byte
  • nibble
  • bit

For the variable types (int, word, char, byte, nibble and bit) it is possible to declare arrays by prefixing the variable name with a *. For example:

bit *bitArray1, bitVar, *bitArray2;

would create 2 bit arrays and a bit variable.

location is always optional and looks like this:

@ ADDRESS

Things imported from other rooms can also be declared if you indicate the room:

TYPE ROOM :: NAME location ;

Again, an address can be specified. It doesn't make much sense in this case, but you can do it.

Here TYPE can be one of:

  • object
  • script
  • cost
  • sound
  • chset
  • voice

Note: For voice, no location can be given, as voices don't have addresses like the other resouces.

3. Room

Room blocks consist of two parts: the room declarations and the room body. The room declarations look like a bunch of C assignments. They define things like background picture, etc. The room body contain resource declarations, objects, scripts, etc.

room NAME location {
  room_declarations
  room_body
}

3.1 Room declaration

There are two types of declaration here: properties and resources. Properties define basic things like pictures, boxes, etc. They are all optional, and when not defined, a default will be used. Resources are used to define costume, charset, etc.

3.1.1 Room properties

The following syntax is used:

PROPERTY = VALUE;

With the following properties avaible:

  • image: the background picture.
  • zplane: mask picture list.
  • boxd: box data for the room.
  • boxm: box matrix.
  • scal: scal slots.
  • trans: transparent color (defaults to 0)

Value can be an integer (trans), a string (image, boxd, boxm, scal) or a list of strings (zplane). The zplane list is as follows:

zplane = { "plane1.bmp", "plane2.bmp" ... }

Note that the files created with boxedit include boxd, boxm and scal together, so to use these files, you only need to define a boxd property.

3.1.2 Room resources

TYPE NAME location = "PATH" , NAME2 location = "PATH2" ... ;

Type can be one of:

  • cost: A costume
  • chset: A charset
  • sound: Some music

PATH is the path to the file containing the resource. location can again be used to enforce the address that will be used for it.

3.1.3 Voices

voice NAME = { "PATH" } ;
voice NAME = { "PATH", sync1, sync2 ... } ;

Voices use a different syntax. The path should point to a .VOC file; the optional sync point list indicates when (in milliseconds) the actor should open/close his mouth.

3.2 Room body

The room body contains variables, scripts or objects. They can have forward declarations in case of interdependence.

room {
  object foo;
  script bar;

  int myVar1;

  object blah {
    ...
  }

  script bar {
    ...
  }
  ...
}

4. Object

Like with rooms, an object block starts with a bunch of properties followed by some scripts.

object NAME location {
  object_properties
  scripts
}

4.1 Object properties

The properties are defined the same way as for rooms.

PROPERTY NAME = VALUE ;

There are the following properties:

  • name : name of the object. This name can be queried by scripts.
  • x and y : location of the object.
  • w and h : size of the object. It's required only if no state is defined.
  • hs_x and hs_y : location of the hotspot. It's used only if no states are defined. Otherwise state 1's hotspot is used for state 0.
  • dir : direction in which actors should be when standing in front of the object.
  • states : defines the various states of the object.
  • state : initial state. Default to 0.
  • owner : actor owning the object. Default is owned by the room.
  • parent : parent object, if any. It must be an object in the same room.
  • parent_state : state the parent must be in for the object to "exist".
  • class : define the object's classes

4.1.1 Object states

By default, objects are in state 0. In this state nothing is displayed, and the object simply defines an area in the room. When set to another state, the corresponding picture and Z planes are used. The state declaration consists of a list of state definitions:

states = { state1 , state2 ... };

With each state definition being:

{
  HS_X, HS_Y,
  IMAGE , { ZPLANE1, ... }
}

The zplanes block is optional. A complete declaration might look like this:

states = {
  {
    17, 42, "object-img1.bmp",
    { "mask1.bmp", "mask2.bmp" }
  },{
    10, 50, "object-img2.bmp"
  }
};

Note that all images and zplanes must have the same size, and both width and height must be multiples of 8.

4.1.2 Object classes

Classes can be used to create various object categories. The is operator (or the isObjectOfClass() function) can then be used to check if an object is part of a class or not. The syntax is:

class = { class1 , class2 ... };

4.2 Object scripts

Object scripts use a structure like a switch, so it's possible to have several verbs using the same code and to do "fall through" from one verb to the next. Note that you must use return to exit the script: even though it looks like a switch, break is not valid in this context.

verb( int this, int that ) {
  variable_declarations
  case VERB1 :
    code
  case VERB2 :
  case VERB3 :
    code
  default:
    code
}

The script can receive any arguments.

5. Scripts

There are two type of scripts: local and global. Local scripts can only be accessed from within the room, a bit like static functions in C. Global scripts, however, are always available.

[local | global] script NAME ( ARGS ) {
  variable_declarations
  code
}

By default, scripts are global. A script can have up to 16 local variables, which are also used to pass arguments to the scripts. Hence the ARGS declaration is just a nice shortcut showing the reader that the script expects to receive arguments. This means that these 2 pieces of code are functionally equivalent:

script foo(int a, int b) {
  ...
}

script foo(int a) {
  int b;
  ...
}

Variables can only be declared at the start of the script; unlike C, it is not possible to declare variables at the start of just any block. You can use any variable type, but internally all local variables are int, so using bit or nibble will not save any memory.

The code consists of instructions. An instruction can be:

  • One or more statements
  • A conditional block
  • A loop block
  • A branching instruction

(Note: a "statement" in this documentation is analogous to an expression in C, and an "instruction" here is analogous to a statement in C.)

Statements and branching instructions must be terminated with a semicolon:

    a = 10 + 50;
    sleep(20);

5.1 Statements

A statement generally has a return value, which can be:

  • A direct value like an integer or a string
  • Any value like a variable or an array
  • An arithmetic or logical expression
  • A function call
  • An assignment

In many places, statements can be put together by separating them with commas. If a return value is needed, the value of the last statement is used. So this code:

  while(a = getFoo(), getCond()) do_job(a);

would continue executing as long as getCond() returns a non 0 value.

5.1.1 Direct values

In SCUMM there are two types of direct (literal) values: integers and strings. Integer values can be written in decimal or in hexadecimal using the 0x prefix. Strings are any text within quotes ("). Some escapes and special sequences can be used:

  • " : A quote
  • \n : A new line
  • \k : Keep text
  • \w : Wait
  • \xNN : A raw byte value in hexadecimal
  • %i{VAR} : Print the integer in VAR
  • %v{VAR} : Print the verb referenced in VAR
  • %V{VOICE} : Play the voice sample VOICE
  • %n{VAR} : Print the name of the object or actor referenced in VAR
  • %s{ARRAY} : Print a string from an array
  • %f{CHARSET} : Set charset (font)
  • %c{INTEGER} : Set color

Strings are directly encoded in the script code, which is quite different from C where they are held in memory. This means you can't pass an array containing a string to a function that expects a string.

5.1.2 Variables

Obviously variables are used by just writing their names. Arrays can be read like this:

  myArray[0] = 45;
  hisArray[1,3] = 78;

Array variables themselves are just like integers but instead they carry the address of the array. However, the arrays have their own address space (like most things in SCUMM), so doing array++ won't lead to the next element in the array like it would in C. If you're lucky, it will go to the next array :)

Some variables are used for communication between the scripts and the engine. You can find a list of them on the Scumm 6 variables page.

5.1.3 List

List are used for a few functions that need a variable number of arguments and to initialize arrays. A list can contain any kind of statement except strings.

  foo(a, [1, 3, 5], 10);
  list[0] = [ 1, 2, fooBar(5), actor5 ];

5.1.4 Operators

The following operators are available:

  • a + b : addition
  • a - b : subtraction
  • a * b : multiplication
  • a / b : division
  • - a : unary minus
  • --a : decrement before eval
  • ++a : increment before eval
  • a-- : decrement after eval
  • a++ : increment after eval
  • a < b : less
  • a <= b : less or equal
  • a > b : greater
  • a >= b : greater or equal
  • a == b : equal
  • a != b : not equal
  • ! a : logical not
  • a || b : logical or
  • a && b : logical and
  • obj is class : object class
  • obj is [class, ...] : object classes
  • a | b : bitwise or (not available in LEC)
  • a & b : bitwise and (not available in LEC)
  • a ? b : c : ternary operator

All of these are like their C counterparts. However, the engine doesn't short-circuit the && and || operators as C code does, which means that in this code:

  if(a && b) did_it();

b would always be evaluated.

Also note that the LEC interpreters don't support the bitwise operators (& and |).

The is operator tests whether an object's class list contains one or more classes. The operand on the right must be either a single class name (not a variable) or a list of class names enclosed in brackets, and each one can optionally be negated by prefixing it with !. If a list is used, the test will return true only if the object belongs to all of the specified classes and none of the negated ones:

  if (objA is [!Pickable,Openable])
    egoSay("I can't pick that up, but I can open it.");

5.1.5 Assignments

The following assignments are possible:

 = += -= *= /= &= |=

(Are &= and |= supported by the LEC interpreter?)

Strings and lists can also be assigned to arrays. A subscript must be given and it indicates where the data should be copied. So such code:

  a[0] = "hello";
  a[5] = " world";
  b[1] = [ 2, 10, 29 ];

will write "hello world" in the array a.

Assignments return the assigned value, allowing constructions like this:

  if((a = getA()) > 10) b = 30;

5.1.6 Function call

Both the engine's built-in functions and user scripts can be called directly:

  setCurrentActor(foo);
  myScript(bar,2);
  someOtherRoom::anotherScript();

Calls to user scripts are just mapped to an equivalent call to startScript0().

A list of the built-in functions can be found on the Scumm 6 functions page.

5.2 Conditionals and loops

Basically all C control blocks are available, plus a few more:

  if ( STATEMENTS ) BLOCK [ else if (STATEMENTS) BLOCK ] [ else BLOCK ]
  unless ( STATEMENTS ) BLOCK ... [ else BLOCK ]

  [ LABEL : ] for ( STATEMENTS ; STATEMENTS ; STATEMENTS ) BLOCK

  [ LABEL : ] while ( STATEMENTS ) BLOCK
  [ LABEL : ] until ( STATEMENTS ) BLOCK

  [ LABEL : ] do BLOCK while ( STATEMENTS )
  [ LABEL : ] do BLOCK until ( STATEMENTS )

  [ LABEL : ] switch ( STATEMENTS ) { case STATEMENT: ... break; }

As you can see, loops can be named, allowing branch instructions (like break and continue) to operate on loops outside the current one. Also note that in switch blocks, the case values can be any statement (unlike in C).

5.2.1 Branching instructions

There are only three of them: break, continue and return.

  waitLoop: while(1) {
    some_stuff();
    while(x < 10) {
      foo();
      if(someTest() == 10) break waitLoop;
      else if(someOtheTest) continue;
      bar();
    }
  }
  return;

5.3 Cutscenes

The VM has some special stuff for cutscenes. First, a script can be automatically called by the VM at the start and end of each cutscene. These are defined with VAR_CUTSCENE_START_SCRIPT and VAR_CUTSCENE_END_SCRIPT. A cutscene block looks like this:

cutscene ( arg1, arg2 ... ) {
  code
}

The arguments are passed to the start and end scripts, if there are any. (Only the first argument is passed to the end script.)

A second thing is the override blocks. These are generally used to make part of the cutscene skippable, but apparently they can also be used outside of cutscene blocks.

try {
  code1

} override {
  code2

}

If the user doesn't do anything, both blocks will be executed, just like if nothing was there. However, if the user presses the skip key while the try block is executing, the engine will directly jump to the override block. When such a jump is done, VAR_OVERRIDE is set to 1, so the script can take special action when the try block is skipped.

6. Costumes

Costumes are currently built with a separate compiler (cost), although later it might be integrated into the main one.

A costume consists of a number of animations, like walking or standing in a given direction. Each animation is made of up to 16 limbs, with each limb having one or more pictures.

6.1 Palette

Costumes can have up to 32 colors. These colors are taken from the room palette.

  palette ( INTLIST ) ;

Where INTLIST is a list of integers or ranges separated by commas. A range is simply [from-to]. For example:

  palette(0, 2, 4, [8-10], 12, 15);

Would define the palette as: 0,2,4,8,9,10,12,15.

6.2 Flipping

Costumes can tell the render to flip the west anims. This allow reusing the images from the east anim and spare some space and work. To enable this set the costume flags after the palette statement:

  flags = FLIP;

6.3 Pictures

Pictures are defined as follows:

  picture NAME = {
    picture_params
    ...
  };

The parameters are then separated by commas. The available parameters are:

  path = "some/path/to/a/file.bmp"

Defines the path of the bitmap to use for this picture.

  glob = "some/unix/glob/file??.bmp"

This one is a bit more tricky. It allows you to define a range of pictures at once. No picture with the name given in the declaration will exist, instead it will automatically use FILE00.BMP, FILE01.BMP, etc.

You can use path OR glob but not both.

  position = { X , Y }

Defines the position of the picture. Both X and Y can be negative, and often are, because you generally want 0,0 to be at the feet of the actor.

  move = { X , Y }

This defines the move field of the actor, but I don't really know what that does.

A complete declaration might then look like this:

  picture walkE = {
    glob = "devil/walkE??.bmp",
    position = { -25, -35 }
  };

6.4 Limbs

Limbs are basically just a big bunch of pictures. The order is important, as animations are defined with indexes from the limb.

  limb NAME location = {
    picture_list
    ...
  };

The location is just like for scripts, etc. Normally you won't need it. The picture list is a list of picture names separated by commas. For example:

  limb body = {
    walkE00, walkE01, walkE02, walkE03, // 0-3
    walkE04, walkE05, walkE06, walkE07  // 4-7
  };

Instead of picture names, you can also use a couple commands:

  • START
  • STOP
  • HIDE
  • SKIP
  • SOUND( INT )

Not much is known about what these commands do, exactly. Sorry.

Note that there can't be more than 16 limbs, so the same limb is generally used for different animations.

6.5 Animations

The final step in defining the costume:

  anim NAME location = {
    direction = { limb_list ... };
    ...
  };

Where direction is one of N,S,E,W. The limb list then defines the limbs used in the animation. A limb entry is defined as follows:

  limbname ( INTLIST ) FLAGS

where the optional FLAGS can be LOOP or !LOOP. By default animations are looped.

A complete definition might look like this:

  anim walk = {
    E = { body([0-7]), head(1,2,5) !LOOP };
    S = { body([10-15]) };
  };

The compiler will recognize a few predefined names and give them the right anim number:

  • init (1)
  • walk (2)
  • stand (3)
  • talkStart (4)
  • talkStop (5)

For a typical costume, you generally want to define at least the init, walk and stand anims.