storm-engine

Game engine behind Sea Dogs, Pirates of the Caribbean and Age of Pirates games.


Project maintained by storm-devs Hosted on GitHub Pages — Theme by mattgraham

Scripting System Overview

back to Index

Storm Engine Logo

Related articles:

Upon startup, the game (ENGINE.exe) will attempt to load and compile the game logic located in program directory. These “scripts” are written in C using the game’s API, and compiled at runtime using the built-in compiler.

Program/ File Structure

Each folder inside program is roughly responsible for its own area of interest as indicated below. In practice, they also serve as databases for related areas of interest: thus, characters will contain all the information and logic related to characters, items will have all the stats on items, etc.

Please note that these do not include any other media than text: all the animations, models, textures, sounds, UI layouts, sounds and videos are located in the resource directory.

The following list is sorted alphabetically and is based on the latest build of Sea Dogs: To Each His Own. Because the contents largely define each game, they differ from one title to another.

Runtime

The game goes through the following phases:

Entities and Layers

The game logic operates on the concepts of “entities”. These are the objects from which compiler can expect some specific attributes. The entities may be tangible like sea, weather, grass, characters, but also intangible like battle interface or sea AI.

For more information on entities, check out this article.

Messages and Events

Events are the central communication method among the entities. Similar to many other languages, it’s a callback system. It allows to register an event with custom name. When the event fires, one or multiple subscribed event handlers will be called and executed.

Information may be passed to events or independently from one entity to another via message system.

For more information on messages and events, check out this article.

Typing

In order to be compiled, the scripting files should contain good C-like code. This also means that the variables should be strongly typed. Commonly used are base C types: int, float, but also bool and a simple to use string. Any custom structs are supported in the form of objects.

All of these types can also be agglomerated inside arrays. The arrays may be declared both in global and local scope, albeit with a few caveats.

Base Types

There’re three base types in scripting language:

syntax: 
    int var_name;
examples:
    int GameTime = -2053; // OK
    int FreeSlots = 75;   // OK
syntax: 
    float var_name;
examples:
    float WindSpeed = 2.0;         // OK
    float WindDirection = -0.0057; // OK
    float ShipDirection = 3.14f;   // OK
syntax: 
    bool var_name;
examples:
    bool IsSpeaking = true;                  // OK
    bool IsDrunk = (AlcoholInBlood > 10.0f); // OK
    bool KilledAtLeastOneEnemy = KillCount;  // OK where KillCount is an int

Strings

In the scripting code, strings are sequences of characters of arbitrary length. You can concatenate multiple strings together using + operator.

syntax: 
    string var_name;
example:
    string Name = "John";                    // OK
    string LastName = GetRandomLastName();   // OK, e.g. "Jameson"
    string FullName = Name + " " + LastName; // OK, "John Jameson"

Arrays

The scripting code allows fixed-size arrays of any type. This means that, after being initialized, the length of the array cannot be modified. However, unlike in C, the array may be initialized from a variable. In that case, the array size would be that variable’s value at the array creation time.

syntax: 
    var_type var_name[int number_of_elements];
example:
    int Characteristics[10];                // OK
    string CharacteristicsNames[10];        // OK

    int array_size;
    array_size = 10;
    string CharacteristicsNames[array_size]; // Also OK

Due to the limitations of the script compiler, a very strict array initialization syntax must be respected.

examples:
    #define FOOD     0
    #define WEAPON   5
    #define MAHOGANY 10

    int ExampleOne[3] = { 0, 5, 10 };               // OK
    int ExampleTwo[3] = { FOOD, WEAPON, MAHOGANY }; // Init OK, but all values are 0!
    
    int ExampleThree[3];
    ExampleThree[0] = FOOD;                         // Illegal
    ExampleThree[1] = WEAPON;                       // Illegal
    ExampleThree[2] = MAHOGANY;                     // Illegal

    int ExampleFour[3];
    ExampleThree[0] = 0;                            // OK
    ExampleThree[1] = 5;                            // OK
    ExampleThree[2] = 10;                           // OK

    void SomeFunction()
    {
        ...
    }

That said, inside the local scope arrays can be assigned values from the #define variables, contrary to the global scope.

examples:
    #define LEATHER 0
    #define TOBACCO 5
    #define PAPRIKA 10

    void SomeFunction()
    {
        int ExampleOne[3] = { 0, 5, 10 };                  // Illegal
        int ExampleTwo[3] = { LEATHER, TOBACCO, PAPRIKA }; // ??
        
        int ExampleThree[3];
        ExampleThree[0] = LEATHER;                         // OK
        ExampleThree[1] = TOBACCO;                         // OK
        ExampleThree[2] = PAPRIKA;                         // OK
        
        int ExampleFour[3];
        ExampleThree[0] = 0;                               // OK
        ExampleThree[1] = 5;                               // OK
        ExampleThree[2] = 10;                              // OK
    }

Objects, References and Attributes

An object is a treelike text structure of an arbitrary shape. This means that it can store any amount of fields in text form, even though specifying a number without quotes is allowed.

Each object can be promoted to ‘entity object’ by binding itself to an entity type (using CreateClass or CreateEntity functions). Entity types are described in detail here.

Uninitialized objects can still be created and used by the scripts, except they won’t be considered engine API.

The strings may be converted back to int or float using sti and stf functions.

syntax:
    object object_name;
example:
    object Sky;
    if (!isEntity(&Sky))
    {
        CreateEntity(&Sky, "Sky");
    }
    Sky.State = "sunset";
    Sky.Size = 10;
    Sky.Angle = "25.0f";

    int skyAngle = stf(Sky.Angle); // 25.0f
    float skySize = sti(Sky.Size); // 10

Objects can also be accessed by reference, defined as ref. Usually it’s useful if you need to access a specific object inside the array.

In order to create a reference, makeref call must be made. After ref has been created, usage syntax is identical to object usage.

syntax:
    object obj_name;
    ref ref_name;
    makeref(ref, obj);
example:
    object SkyStates[SKY_STATES_N];
    ref Sky;
    int n = 0;

    // iterate over all sky states    
    makeref(Sky, SkyStates[n]);
    Sky.State = "sunset";
    Sky.Size = "10";
    Sky.Angle = "25.0f";
    n++;
    // rinse and repeat

Finally, you can access a single attribute of an object using an attribute, or aref, variable. Similarly to the ref, makearef call must be made to store the attribute. Since each object can store multiple objects, arefs can be quite helpful.

syntax:
    obj obj_name;
    aref aref_name;
    makearef(aref, obj);
example:
    object Item;
    aref  ItemType;

    Item.type = "sword";

    makearef(ItemType, Item.type);

If you are accessing an attribute via a variable, you should put that variable inside brackets:

example:
    aref aSky;
    string attribute = "d20";
    aSky.Dir.d20 = "NNE";         // OK
    aSky.Dir.(attribute) = "NNE"; // OK
    aSky.Dir.attribute = "NNE";   // Error

Predefined attributes of the entity objects will be interpreted by the engine at runtime.

example:
    object sky;
    if (!isEntity(&sky))
    {
        CreateEntity(&sky, "Sky");
    }
    sky.State = "sunset";   // Affects game engine directly
    sky.Size = 10;          // Affects game engine directly
    sky.Angle = "25.0f";    // Affects game engine directly
    sky.Coverage = "clear"; // Does not affect the game engine directly, 
                            // probably used elsewhere in the scripts

Branching

Script compiler supports the common branching options: if and switch statements, while and for loops, goto jumps. However, due to some scripting engine properties, there’re some caveats in the usage.

Logical Comparisons

The usual operators can be used to determine a boolean value:

// TODO(yakvi): Info below is dated 2005. Is this bug still relevant?

Multiple comparisons may be chained using logical AND (&&) and OR (||) operators. Inline cal of the functions returning a bool is also supported. However, operator mixing of these is not allowed.

example:
    bool a = true;
    bool b = false;
    bool c = false;

    if (a && b) // OK
    {
        // ...
    }

    if (a && b && doWork()) // OK
    {
        // ... 
    }

    if (a && (b || c)) // Illegal
    {
        // ...
    }

That said, chaining results of multiple operators is allowed, as well as nesting.

example:
    bool a = true;
    bool b = false;
    bool c = false;

    if (a && (b || c)) // Illegal
    {
        // ...
    }

    bool bc = b || c;
    if (a && bc) // OK
    {
        // ...
    }

    if (a) // OK
    {
        if (b || c) // OK
        {
            // ... 
        }
    }

Last but not least, the scripting engine supports expression optimization. If an expression’s result can be determined earlier (e.g. the first value in an OR chain is true), following values will not be evaluated.

example:
    bool a = false;
    bool b = true;
    bool c = false;
    
    if (a ||       // false
        b ||       // true
        c ||       // ignored
        doWork())  // skipped
        {
            // doWork will only be executed if a, b, c are false.
            // ...
        }

    if (a &&      // false
        doWork()) // skipped
        {
            // ...
        }

if statement

The code inside the if statement block will be executed if the condition is true. Optional else block is also supported.

syntax:
    if(condition)
    {
        // code if true
    }
    else
    {
        // code if false
    }

switch statement

The code inside the switch block is evaluated, and code is executed based on specific constants. default case is not supported.

syntax:
    switch (variable)
    {
        case CONSTANT_1:
            // code
            break;
        case CONSTANT_2:
            // code
            break;
        case CONSTANT_3:
            // code
            break;
        // etc.
    }

while loop

The code inside the while loop will run forever until the condition becomes false.

syntax:
    while(condition)
    {
        // code while condition is true
        // remember to add an exit condition!
    }

Additionally, continue and break keywords are implemented and work as expected.

example:
    int i = 0;
    bool isRunning = true;
    while(isRunning)
    {
        // do so work
        if (someEvent)
        {
            i++; 
            continue; // go immediately to the next loop iteration
        }
        
        if (i > 20)
        {
            break; // go out, code below won't be executed
        }

        // more work
    }

for loop

A for loop behaves as expected. It’s similar to a while loop in running until a condition is false but it also has two other blocks for initializing an iterator and increasing its value.

syntax:
    for(int_var = init_value; condition; increase/decrease int_var)
    {
        // code while condition is true
    }
example:
    for (int i = 0; i < 20; i++)
    {
        // do some work 20 times
    }

goto statement

The goto statement allows jumping to a label specified above or below the goto.

syntax: 
    goto label_name;
    // ... 

    :label_name; 

example:
    :label_above;

    // ...
    if (someEvent)
    {
        goto label_above; // OK
    }
    else
    {
        goto label_below; // OK
    }

    :label_below;
    // ...

Other Considerations

Functions

Functions are implemented similarly to C. Each function can have several parameters as input and a return type (which can be void if there’s nothing to return) as output.

syntax: 
    return_type FunctionName(parameter_type Param, ...) 
    {
        // function body
    }    
example:
    void DoWork() 
    {
        // ...
    }
    
    bool IsItRaining(float timeOfDay)
    {
        // ... 
    }

You can consult the list of built-in functions here.

Comments

Both single line // and multi-line comments /**/ are supported.

syntax: 
    // single-line comment
    /* 
        multi-line comment
    */

Original article authors

ALexusB, 22.04.05

Warship, 02.08.09


Home Site Map