Phantasmal MUD Lib for DGD

Phantasmal Site > DGD > Untitled document (index.base.html) > (untitled)

Conditionals

Conditional statements are used a lot in LPC, and there are several ways of writing them. A very important concept is that 0 and nil are considered as false and any other value as true in tests. This means that empty listings ({}), empty strings "" and empty mappings ([]) also are evaluated as true since they aren't 0 or nil. You have to use special functions to compute their size or determine content if you want test them.

The if and else statements

The most common conditional statement is the if statement. It's easy to use and can be combined with an else clause to do two different things based on a variable's value. It's written like this:

if (expression) statement;
e.g.
    if (a == 5)
        a -= 4;
    

If you want to handle the false case, you can add an else statement like this:

if (expression) true-statement else false-statement;
e.g.
    if (a == 5)
        a -= 4;
    else
        a += 18;

or

    if(a > 10)
      a -= 10;
    else {
      a += 100;
      b--;
      a -= 10;
    }
    

The switch statement

If one variable has to be tested for a lot of different values, the resulting list of `if-else-if-else' statements soon gets very long and hard to read. However, if the type of the value you are testing is an integer, a float or a string you can use a much denser and neater way of coding. Assume you have the following code you want to write:

if (name == "fatty")
{
    nat = "se";
    desc = "blimp";
}
else if (name == "plugh")
{
    nat = "no";
    desc = "warlock";
}
else if (name == "olorin")
{
    nat = "de";
    desc = "bloodshot";
}
else
{
    nat = "x";
    desc = "unknown";
}
    

A better way of writing this is:

switch (name)
{
case "fatty":
    nat = "se";
    desc = "blimp";
    break;

case "plugh":
    nat = "no";
    desc = "warlock";
    break;

case "olorin":
    nat = "de";
    desc = "bloodshot";
    break;

default:
    nat = "x";
    desc = "unknown";
}
    

The workings of this statement is simple: switch sets up the expression value within the parenthesis for matching. Then every expression following a case is compared to find a match.

Note that the case expression must be a constant value. It can't be a variable, function call or other expression.

After a match has been found the following statements are executed until a break statement is found. If no matching value can be found, the default statements are executed instead.

While it's not mandatory to have a default section, it's highly recommended since that usually means that something has happened that wasn't predicted when writing the program. It's usually very good to have an error message to notify the user (or you) that something unexpected happened.

If you forget to put in a 'break' statement the following 'case' expression will be executed. This might sound like something you don't want, but if in the example above the names `fatty' and `plugh' both should generate the same result you could write:

case "fatty":
    /* FALLTHROUGH */
case "plugh":
    < code >
    break;

... and save a bit of space. Writing code with switch doesn't make it any quicker to execute. It does make it a lot easier to read, which reduces the chance of making mistakes while coding. Remember to put the /* FALLTHROUGH */ comment there though, or it might be hard to remember later if it was intentional or an omission of a break statement. If you have code that's executed before falling through to the next case, this is especially important. A good idea is usually to add an extra linefeed after a break statement just to give some extra 'breathing space' to improve on legibility.

The ?: expression

This is a very condensed way of writing an if/else statement and return a value depending on how the test turned out. The ?: operator isn't a statement, it's an expression since it returns a value. It's listed here instead of being listed among the expressions because it's effectively a conditional, though.

Suppose you want to write the following:

if (test_expression)
    var = if_expression;
else
    var = else_expression;
    

You can write that in a much more condensed way:

var = test_expression ? if_expression : else_expression;
e.g.
    name = day == 2 ? "tuesday" : "another day";
    

Opinions vary as to whether writing the conditional with this operator makes the code easier or harder to read. A common rule of thumb is that one use of the ?: operator makes code clearer, but that several in a single statement only makes it worse. Something like this definitely isn't an improvement:

name = day == 2 ? time == 18 ? "miller time" : "tuesday" : "another day";
    

Loop statements

There are two loop statements in LPC which incorporate the use of conditional statements within them. That means they can be programmed to execute only until a certain condition is true.

The for statement

If you want a counter or an iterator, you should usually use the for statement. The syntax is as follows:

for (initalize_statement ; test_expression ; end_of_loop_statement)
    body_statement;
    
When first entered, the for statement executes the initialize_statement part. This part usually is used to initialize counters or values for the loop itself. Then the first loop starts. Every loop starts by executing the test_expression and examining the result. This is a truth conditional, so any answer not equal to 0 or nil will cause the loop to be run. If the test expression is true then the body_statement is executed, immediately followed by the end_of_loop_statement. In the body_statement you usually do what you want to have done for this iteration. In the end_of_loop_statement you usually increment or decrement counters as needed to prepare them for the test_expression in the next loop.

Throughout the previous section I used the word usually a lot. This is because you don't have to do it that way, there's no rule forcing you to make use of the statements in the way I said. For the moment let's stick to the regular way of using the for-statement. Later on I'll describe more refined techniques, and you can discover your own as well.

Assume you want to compute the sum of all integers from 7 to 123 and don't know the formula ((x2^2 + x1^2) / 2). The most straightforward way of doing that is a loop.

result = 0;
for (count = 7 ; count < 124 ; count++)
    result += count;
    

First of all, result is set to 0. Then the actual for statement is entered. It begins by setting the variable count to 7. Then the loop is entered, beginning by testing if count (= 7) is less than 124. It is, so result has count added to it. Then count is incremented and the loop is entered again. This goes on until the count value reaches 124. Since that isn't less than 124 the loop is ended.

The loop form above is pretty standard in C, but you may have realized there's another way you can write the same thing:

result = 0;
for (count = 7 ; count <= 123 ; count++)
    result += count;
    
This way works fine too, and you may find it more understandable. Then again, you may not.

Please note that the value of count after the for statement will be 124 and not 123. The test_expression must evaluate to false in order for the loop to end, and in this case the value for count then must be 124. This is true for both ways of writing the loop above!

The while statement

The while statement is pretty straightforward. You can probably guess from its name what it does. The statement will perform another statement over and over until a given while expression returns false. The syntax is:

while (<test expression>)
    

Note carefully that the test expression is checked first of all, before running the statement the first time. If it evaluates as false the first time, the body is never executed.

a = 0;
while (a != 4)
{
    a += 5;
    a /= 2;
}
    

The break and continue statement

Sometimes during the execution of switch, for or while statements it becomes necessary to abort execution of the block code, and continue execution outside. To do that you use the break statement. It stops the execution of that block and continues after it.

while (end_condition < 9999)
{
    /* If the time() function returns 29449494, abort execution */
    if (time() == 29449494)
        break;

    < code >
}

/* Continue here both after a break or when the full loop is done. */
< code >
    

Sometimes you merely want to start over from the top of the for or while loop you're running. To do that, you use the continue statement.

/* Add all even numbers */
sum = 0;
for (i = 0 ; i < 10000 ; i++)
{
    /* Start from the top of the loop if 'i' is an odd number */
    if (i % 2)
         continue;

    sum += i;
}
    
Notice that the i++ is executed when the loop is continued. Only the sum += i; is skipped.

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");
}
    

The preprocessor

The preprocessor isn't a part of the LPC language proper. It's a special process that is run before the actual compilation of the program occurs. It can be seen as a very smart string translator. Specified strings in the code are replaced by other strings.

All preprocessor directives are given as strings starting with the character # on the first non-whitespace column of the line. It's considered good practice to put preprocessor directives on the very far left of the code, with the # in the very first column.

The #include statement

This is by far the most common preprocessor command. It simply tells the preprocessor to replace that line with the contents of an entire other file before going any further.

Data you put in included files is usually data that won't ever change and that you'll be referencing in several files. Instead of having to copy and paste the same lines into multiple places and maintaining multiple copies, you simply collect that data in an include file and include it in the program files as appropriate. Included file names traditionally end in .h.

The syntax for inclusion is simple:

#include <standard_file>
#include "special_file"
    

Note the absence of a ; after the line.

There are two different ways to write this. Which you use depends on where the file is that you want to include. There are a usually standard include files which may be in any of several different directories. Rather than having to remember exactly where they are, you can just give the name of the file you want to include if it's in a standard include directory.

#include <limits.h>
#include <types.h>
    

If you want to include files that aren't in the standard include path, for example files of your own, you have to specify where they are. You can do that either relative to the position of the file that uses it or by an absolute path. (NOTE: does DGD allow non-absolute include paths?)

#include "/d/Genesis/login/login.h"
#include "my_defs.h"
#include "/sys/adverbs.h"     /* Same as the shorter one above */
    

When you include standard files, always use the <>-path notation. Not only is it shorter and easier to distinguish, but also if the files move around then files included with relative or absolute syntax won't be found. If you use the special include syntax then they will be found in any standard directory, even if they move between them.

It's possible to include LPC files -- entire files full of code. Doing so is normally considered very bad form. Error handling usually has a bad time tracing errors in included files -- there are frequently problems with line numbers. Since you include the uncompiled code into several different objets, you will waste memory and CPU for these identical functions and variables. It also tends to be harder to figure out where functions are defined.

What does the extension of the file name really have to do with the contents then? Technically, nothing at all. But the convention is to keep functions in .cc files and definitions in .h files. Many mudlibs enforce this, and accept only certain file suffixes for compilation or other tasks.

The #define statement

#define is a very powerful macro or preprocessor command that can be abused endlessly. It's recommended that you use it with caution and only for simple tasks. Using it for complex tasks tends to make reading or debugging your code quite difficult.

The syntax is as follows:

#define <pattern> <substitute pattern>
#undef <pattern>
    

Any text in the file that matches <pattern> will be replaced with <substitute pattern> before compilation occurs. A #define is valid from the line it is found on until the end of the file or an #undef command that removes it.

Although the preprocessor allows #define labels to be any sort of legal text, it is customary to use only capital letters. This is so that they will be easily distinguishable as what they are.

Place all defines in the beginning of the file. Again, the compiler doesn't enforce this but it's a very good idea. It guarantees not only that they are easy to find, but also that they're usable throughout the file.

Common defines include paths, names and above all constants of any kind. By defining them, you avoid writing them over and over.

#define MAX_LOGIN  100          /* Max logged on players */
#define MY_USER    "/usr/System/my_user" /* My user object */
#define GREET_TEXT "Welcome!"   /* The login message */
    

Anywhere the pattern strings above occur in the file where they're defined (and any file that includes it), they will be replaced by the defined value above. The substitution includes the comments above, but they're ignored by the compiler.

DRIVER->message(GREET_TEXT + "\n");
    

If a macro extends beyond the end of the line you can terminate the lines with a \. That continues the macro onto the next line. However, you must put the newline immediately after the \. There may not be spaces or other characters between the backslash and the newline.

#define LONG_DEFINE  "beginning of string \
                      and end of the same" 
    

Function-like defines are fairly common and often abused. It's important to write macros such that every argument to the macro is enclosed in parenthesis where it's used. If you don't do that you can end up with some very strange results.

1: #define MUL_IT(a, b) a * b        /* Wrong */
2: #define MUL_IT(a, b) (a * b)      /* Not enough */
3: #define MUL_IT(a, b) ((a) * (b))  /* Correct */
    

What's the big difference? Look at this example:

result = MUL_IT(2 + 3, 4 * 5) / 5;

   Expanded with the three different macros, this becomes:

1: result = 2 + 3 * 4 * 5 / 5;       /* = 14, Wrong */
2: result = (2 + 3 * 4 * 5) / 5      /* = 12, Still wrong */
3: result = ((2 + 3) * (4 * 5)) / 5  /* = 20, Correct! */
    

Common problems with defined constants and functions include badly formulated macros, complicated macros used inside other macros (making the code almost impossible to understand) or humongous arrays or mappings in defines that are used often. The basic rule is to keep macros short and fairly simple.

The #if, #ifdef, #ifndef, #else and #elseif statements

The commands above are all preprocessor directives aimed at selecting certain parts of code and perhaps removing others depending on the state of a preprocessor variable.

The #if statement looks very much like a normal if statement, but is written a bit differently.

Assume you may have one of the following definitions somewhere:

#define code_VAR  2

or
        
#define code_VAR  3
    

Then you can write

#if code_VAR == 2
    <code that will be kept only if code_VAR == 2>
#else
    <code that will be kept only if code_VAR != 2>
#endif
    

You don't have to have the #else statement there at all if you don't want to. You can simply use #if and #endif.

It's sufficient to have the following statement to 'define' a preprocessor pattern as existing:

#define code_VAR    /* This defines the existance of code_VAR */
    

Then you can use #ifdef to check for its existence, like this:

#ifdef code_VAR
    <code that will be kept only if code_VAR is defined>
#else
    <code that will be kept only if code_VAR isn't defined>
#endif

or you can use #ifndef, which is essentially an #ifdef with an extra "not" implied.

#ifndef code_VAR
    <code that will be kept only if code_VAR isn't defined>
#else
    <code that will be kept only if code_VAR is defined>
#endif
    

Again, the #else is optional.

The #if/#ifdef/#ifndef preprocessor commands are frequently used to add debug code that you don't want to have activated all of the time, or code that will work differently depending on other very rarely changing parameters. Since the conditions have to be hard-coded in the file and can't change easily at runtime, most features will use a regular if instead of an #if.