Phantasmal MUD Lib for DGD

Phantasmal Site > DGD > DGD Reference Manual > LPC > Data Types and Variables

Data Types and Variables

An object holds information in variables. Variables are a sort of virtual container that holds information. It's called a variable because the information is allowed to change later. Most objects process information with functions. Functions can use and return data of various kinds.

In principle only one kind of data type is needed, a sort of general container that would hold anything you wanted it to. LPC calls this type 'mixed'. Usually, though, it's much more useful if you can distinguish between different types of information. Knowing what's in a variable can be a very good thing. It greatly improves on the time it takes to write and debug an object.

In LPC it is possible to use only data of type 'mixed'. In the first versions of the language, that was the only data type available. With modern LPC, however, it's better to avoid mixed variables when you can.

LPC lets you declare variables of these types:

void
Void is only used as the return type of functions that don't return any data at all. You'll never declare a variable of type void, though you'll use it like other types.
int
Integers (LPC calls them 'int' for short) are whole numbers, normally between -2147483648 and 2147483647. For instance 3, 17, -32, and 999 are all perfectly good integers. You can write integers in a couple of different ways, such as in hexadecimal (base 16), but they're still the same data type underneath, they're just written differently.
float

Fractional numbers, usually approximately between 1.17549435e-38 and 3.40282347e+38. For instance 1.3, -348.4, 2.0 and 4.53e+4 are floating-point numbers (LPC calls them 'float'). In case you're not familiar with Scientific Notation for numbers, 4.53e+4 is the same as 4.53 x 10^4, 4.53 * 10000, or 45300.

LPC doesn't recognized numbers like 1. or .4711 as floats. You have to specify both an integer and a decimal part for every number, even if they're zero for the number you're typing. LPC's just funny that way.

string
Strings are simply a series of printable characters. They're written with the characters in double quotes. For instance "x", "the string", "Another long string with the number 5 in it". Strings can contain special characters like newline (written as "\n") to end a line. A lot of LPC operators can handle strings directly, unlike in C. For instance, you can put two of them together with +. This makes strings much easier to use.
mapping
Mappings are another handy LPC feature. A mapping is simply a list of associated values. Assume you want to remember the ages of people. Say that Olle is 23, Peter is 54 and Anna is 15. In LPC you can turn this into the mapping ([ "Olle":23, "Peter":54, "Anna":15 ]). The value to the right has been associated to the key to the left. You can then find the value by looking for the key. DGD lets you do intersections and unions and other operations on mappings.
object
Object pointers are references to LPC programs that have been loaded into memory. The object pointer may point to an original object, or to a clone.
Array
All of the above can appear in arrays, indicated by a * in front of the variable name in the declaration. Arrays in LPC are more like lists than arrays in C. LPC and DGD let you do unions and intersections and other array operations that most languages don't.
mixed
Type mixed is a general descriptor covering all the others, a sort of jack-of-all-trades type. An object of any other type may be stored within a mixed type. But when a regular type can be used, it should. Don't use mixed just because you don't want to remember the type's name. The type system is there to catch errors for you. Let it.

There's also a special value called nil, which is not really any of the types above. A string, mapping, array or mixed variable that isn't initialized has a value of nil. A freshly-allocated array is often allocated to nil. You can do some tricks like assigning nil to an entry in a mapping to remove it. Nil is like an undefined value, or a zero for things that aren't necessarily numbers. You'll see more of it in examples later on.

If you need to know the limits of integers, characters or floating-point numbers, you can check DGD's include/limits.h and include/float.h files. They list limits of the various data types. Bear in mind that DGD can easily be compiled with different integer and floating point limits, so it's good to make your code check the sizes. They may be different next time your program runs!

Variable declarations

A variable is a string of letters identifying an information container, a place to store data. The container is given a name consisting of 32 characters or less, starting with a letter. No special character other than the '_' used to separate words is ever used. Variables should always be given names that reflect how they are used. You declare variables like this:

<data type> <variable name>, <another variable>, ..., <last variable>;
e.g.
    int        counter;
    float      height, weight;
    mapping    age_map;
    

Variables must be declared at the beginning of a block, immediately after the first { and before any other statements. Global variables, variables that are available in all functions througout the program, should be declared at the top of the file.

Variables are initially set to 0 or to nil, and not necessarily to the obvious 'empty' values. Mappings, arrays and strings will all be set to nil and not to ([]), ({}) or "" as you might expect.

Arrays and mappings should be initalized to their empty values (({}) and ([]) respectively) before being used. You can't add a value to a mapping that's set to nil, for instance, and you'll get a runtime error if you try.

Arrays and Mappings

It's time to dig deeper into the special types array and mapping. For each of these data types there exist a number of useful functions and operators that manipulate them and extract information from them. Some of those functions and operators won't be described in more detail until later.

How to declare and use arrays

Arrays really aren't arrays in the proper sense of the word. They can better be seen as lists with fixed order. The difference is that arrays can't usually be manipulated easily, while LPC arrays have a rich set of operations to reorder them, insert into them and otherwise manipulate them.

Arrays are type-specific. This means that an array of a certain type only can contain variables of that single type. All arrays are one-dimensional, which means you can't declare an array which is like a 2- or 3-dimensional grid rather than a list. However, the mixed type takes care of these limitations. You can also declare an array of arrays, which also takes care of the problem. A mixed variable can act as an array containing any data type, even other arrays. As a rule you should try to use properly typed arrays to minimize the probabilities of programming mistakes. When that's not possible, though, you can use the mixed type.

You declare an array like this:

<type> *<array name>;
e.g.
    int *my_arr, *your_arr;
    float *another_arr;
    object *ob_arr;
    

The initial values of these declared arrays is nil, not an empty array. I repeat: they are initialized to nil and not to an empty array. Keep this in mind!

You can allocate and initialize an array like this:

<array> = ({ elem1, elem2, elem3, ..., elemN });
e.g.
    int *my_arr;

    my_arr = ({ 1, 383, 5, 391, -4, 6 });
    

You can allocate an array of type mixed with the allocate function, like this:

<array> = allocate(<num of elements>);
e.g.
    mixed *some_array;

    some_array = allocate(4);
    some_array[0] = "Upper Slavonia";
    some_array[1] = ({ 1, 2, 7, 9 });
    some_array[2] = ([ "bob" : 7 ]);
    some_array[3] = 7;
    

To access members of the array, use brackets after the variable name:

<data variable> = <array>[<index>];
e.g.
    val = my_arr[3];

    val2 = some_array[1][3];
    

LPC, like C, starts counting from array index 0. That means the index to the fourth value in an array is 3.

To set the value of an existing position to a new value, simply set it using the = operator.

    my_arr[3] = 22;     /* => ({ 1, 383, 5, 22, -4, 6 }) */
    my_arr[3] = 391;    /* => ({ 1, 383, 5, 391, -4, 6 }) */
    

If you want to copy a subset of an array you can specify a range of indices within the brackets. This is called an array slice.

<array variable> = <array>[<start_range>..<end_range>];
e.g.
    your_arr = my_arr[1..3];
    
This will result in your_arr becoming the new array ({ 383, 5, 391 }); If you give a new value to an old array, the previous array is lost.
e.g.
    my_arr = ({ });
    

This code will result in my_arr holding an empty array. The old array is deallocated and the memory previously used is reclaimed by the driver.

If you index outside an array, an error occurs and execution in the object is aborted. However, range indexing outside the array does not result in an error, the range is then only constrained to fall within the array. So, for instance, if my_arr is an empty array, the code my_arr[3..7] will return an empty array.

Concatenating (adding) arrays to each other is most easily done with the + operator. Simply add them as you would numbers. The += operator works fine as well.

my_arr = ({ 9, 3 }) + ({ 5, 10, 3 }); /* => ({ 9, 3, 5, 10, 3 }) */
    

Removing elements from an array is most easily done with the - or -= operator. Be aware that the operator that will remove all items found that match the item you want to remove, not just one.

my_arr -= ({ 3, 10 }); /* => ({ 9, 5 }) */
    

If you want to remove a single item somewhere in the array that might have been repeated, you should use the range operator.

my_arr = ({ 9, 3, 5, 10, 3 });
my_arr = my_arr[0..0] + my_arr[2..4]; /* => ({ 9, 5, 10, 3 }) */
    

Be careful of the following difference. One is a list, the other an integer:

    <array> my_arr[0..0]   /* = ({ 9 }) */
    <int>   my_arr[0]      /* = 9 */
    

This means that if you wrote the above code as follows, it wouldn't work:

my_arr = my_arr[0] + my_arr[2..4];
    

Instead, it would warn you that you can't add an integer to an array.

You can leave one end of the array slice unspecified, which will use the beginning or end of the array. For instance:

my_arr = my_arr[..2] + ({ 3, 7, 4 }) + some_array[3..];
    

The code above would use elements 0 through 2 of my_arr, add ({ 3, 7, 4 }) to the end, and then add all elements of some_array, starting at the fourth.

How to declare and use Mappings

Mappings are lists of associated values. They are of mixed type, meaning that the different indices and the different values can be of varying types within the same mapping.

Mappings can use any kind of data type for either index or value. The index part of the mapping in a single mapping must consist of unique values. There cannot be two indices of the same value as one would overwrite the other.

You can declare a mapping just like any other variable, so let's start with a few declarations for later use:

mapping my_map;
int     value;
    

Allocating and initializing can be done in two different ways:

1:  <mapping_var> = ([ <index1>:<value1>, <index2>:<value2>, ... ]);

2:  <mapping_var>[<index>] = value;
    

The first is straightforward and easy.

1: my_map = ([ "adam":5, "bertil":8, "cecar":-4 ]);
    

In the second case, if a given index doesn't exist in the mapping then it is created when referenced. If it does exist then the value for that index is replaced with the one being assigned.

2: my_map["adam"] = 1;    /* Creates the pair "adam":1 */
   my_map["bertil"] = 8;  /* Creates the pair "bertil":8 */
   my_map["adam"] = 5;    /* Replaces the old value in "adam" with 5. */
        ...
    

Unlike arrays there's no order in a mapping. The values are stored in a way that makes finding the values as quick as possible. There are functions that will allow you to get the component lists (the indices or values) from a mapping but keep in mind that they can be in any order and are not guaranteed to remain the same from call to call. In practice they only change order when you add or remove an element, but it's best not to rely on that.

You can merge mappings with the + and += operators, just as with arrays.

my_map += ([ "david":5, "erik":33 ]);
    

Removing items in a mapping is simple. You can assign nil to that index of the array. Doing so will delete the index/value pair:

my_map["david"] = nil;
    

Individual values can be obtained through simple indexing:

value = my_map["cecar"]; /* => -4 */
    

Indexing a value that doesn't exist will not generate an error, only the value nil. Be very careful of this since you might indeed have legal values of nil in the mapping as well -- for instance, a value of nil might mean that the index has no value part, or instead that the value indeed is nil:

value = my_map["urk"]; /* => nil */
    
If you need to be certain, there is a function called map_indices which will return the list of indices. You can check to see if the index exists that way:
if(map_indices(my_map) & ({ "urk" })) {
    DRIVER->message("Urk is a member of the array!\n");
}
    

Scope and prototypes

Scope is a term defining where a function or variable declaration is valid. Since programs are read top down (just like you read this page), declarations of functions and variables are available to the below the actual declaration. However, the scope might be further limited.

A variable that is declared inside a function is only valid until the end of that function (the terminating }) is reached. If it's declared in a block inside that function, such as inside a for loop, it's only valid until the end of that block (the } matching the beginning { of that block).

<top of file>
int GlobCount;

/* Only GlobCount is available here */

void
var_func(int arg)
{
    int var_1;

    /* GlobCount, arg and var_1 are available here */
    < code >

    {
        string var_2;

        /* GlobCount, arg, var_1 and var_2 are available in this block */
        < code >
    }

    /* GlobCount, arg and var_1 are available here */
    < code >

    {
        int var_2;
        mapping var_3;

        /* GlobCount, arg, var_1, var_2 and var_3 are available here
          Note that this var_2 is a NEW var_2, an int not a string */
        < code >
    }

    /* GlobCount, arg and var_1 are available here */
    < code >
}

/* Here only GlobCount and the function var_func are available */
    

Function declarations follow the same rule of scope, though you can't declare a function inside another function. Suppose you have these two functions where the first uses the second:

int
func_1()
{
    < code >
    func_2("test");
}

void
func_2(string data)
{
    < code >
}
    

Then you have a problem, because the first function tries to use the second function before it is declared. This may result in an error message, and it's bad practice in any case. To take care of this you can rearrange the functions so that func_2 comes before func_1 in the listing. This isn't always the best layout, and it isn't always possible. It's usually better to write a function prototype. The function prototype should be placed at the top of the file after the inherit and #include statements (described later) but before any code. It should look very much like the function declaration itself. In this case:

< top of file, inherit and #include statements >

void func_2(string data);

< the actual code >