Phantasmal MUD Lib for DGD

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

Essential LPC

This chapter will show you some more involved examples, and hit some of the more interesting features of LPC that set it apart from similar languages like C.


4.1 A Peek at things to come

To present things on the screen for the player to read, the message() function is called on that player's user object. Different mudlibs supply the user object in different ways.

There are two special characters that are often used to format text, tab and newline. They are written as \t and \n respectively within DGD strings. The tab character inserts 8 space characters and newline breaks the line.

void message(string text);
e.g.
    user->message("Hello there!\n");
    user->message("\tThis is an indented string.\n");
    user->message("This is a string\non several lines\n\tpartly\nindented.\n");

    /* The result is:

       Hello there!

               This is an indented string.

       This is a string
       on several lines
               partly
       indented.
     */
    

LPC revisited

Let's go through more of the LPC constructs that we didn't finish in the LPC Basics section. Now that you know the basics and can use them for simple scripts, it's time to find out more of what LPC can do.

Function calls

There are two kinds of function calls, internal and external. We've only discussed internal calls so far.

Making object-internal function calls

Making an internal function call is as simple as writing the function name and putting any arguments within parentheses afterwards. The argument list is simply a list of expressions, or nothing. A function call is an expression if it returns a value. A function call followed by a ; is a statement.

<function>(<argument list>);
e.g.
    pi = atan(1.0) * 4;
    

Making single object-external function calls

An external call is a call from one object into another. In order to do that you need an object reference to the object you want to call. We haven't discussed exactly how you acquire an object reference yet, but assume for the moment that it already is done for you. Object references (the same thing as object pointers) can be stored in variables of type object.

mixed <object reference/object path>-><function>(<argument list>);
mixed call_other(<ob ref/ob path>, "<function>", <arg list>);
e.g.
    /*
     * Assume that I want to call the function 'compute_pi' in the
     * object "/d/Mydom/thewiz/math_ob", and that I also have the
     * proper object pointer to it stored in the variable 'math_ob'
     */
    pi = math_ob->compute_pi(1.0);
    pi = "/d/Mydom/thewiz/math_ob"->compute_pi(1.0);
    pi = call_other(math_ob, "compute_pi", 1.0);
    pi = call_other("/d/Mydom/thewiz/math_ob", "compute_pi", 1.0);
    

Note that you can either use the call_other function or just use the -> notation. One is syntactic sugar for the other. The behavior is identical either way. Note that if you use the call_other() form, you need to put quotes around the function name since you're actually supplying any string -- which can be a variable, or the result of a computation:

pi = call_other(math_ob, "computer" + "_pi", 1.0);
    

If the object you call hasn't been loaded into memory yet, it will be. If it has a create() function that hasn't been called, that function will be called (note that the create() function may have a different name if DGD is configured differently). If an external call is made to a function that doesn't exist in the object you call, nil will be silently returned without any error messages.

If you use an object path (a string) instead of an object reference, the master object will be called. The master object can contain data just like any of the clones, and is often a good place to store a central copy of information that all clones want to use without each one needing its own copy.

Call-by-value versus Call-by-reference

In LPC, most values are passed by value. What that means is that when you pass, say, an integer variable with a value of 3 into a function and you change that variable in the function, the variable doesn't change outside the function . So the following code doesn't do what it looks like it's supposed to. In fact, it does nothing:

/* Swap two integers */
void swap(int a, int b) {
  int tmp;
  tmp = b;
  b = a;
  a = b;
}
    

The reason it doesn't work is because LPC makes the function's parameters new variables and copies the values in. You're swapping the value of the parameters, but those variables are going to disappear at the end of the function. This is called call-by-value because only the value gets passed in -- the original variable is copied so the function never sees it. In C, we could use pointers to pass references. In other languages, we could do what's called call-by-reference, and the swap routine above could actually swap the values of the two variables once the function was finished. LPC passes arrays by reference. That means that if you change an element in the array, it's changed everywhere, not just in that one function.

LPC passes several types by reference: arrays, mappings and objects. A mixed type is passed either by value or reference depending on what type it "really" is underneath.

This has security implications. For instance, let's say a function on your MUD keeps an array of all users. When queried, it passes the array of users to a calling function. But a wizard on your MUD would be able call that function and remove other users from the array. That's a bad thing. So instead, you just copy the array before returning it. If you just pass the array back as an array or mixed variable, it's call-by-reference and the wizard can change your copy. If you copy it first, it doesn't matter how it's passed because he's looking at a different copy.

Inheriting object classes

Assume that you want to code a door. Doing that means that you have to create functionality that allows the opening and closing of a passage between two rooms. Perhaps you want to be able to lock and unlock the door, and perhaps you want the door to be transparent. All of this must be taken care of in your code. Furthermore, you have to copy the same code and make small variations in description and use every time you want to make a new door.

After a while you'll get rather tired of this, particularly because you'll find that other wizards have created doors of their own that work almost - but not quite - the same way your does, rendering some of your nifty objects and features useless anywhere but in your domain.

The object oriented way of thinking is that instead of doing things over and over, you create a basic door object that can do all the things you want any door to be able to do. Then you just inherit this generic door into a specialized door object where you configure exactly what it should be able to do from the list of available options in the parent door.

It is even possible to inherit several different objects in order to combine the functionality of several object types into one. Be aware that if a class's parent objects define functions with the same names, they will clash. It may not be easy to fix this problem, so avoid inheriting from more than one parent until you're reasonably sure what you're doing.

The syntax for inheriting objects is very simple. In the top of the file you write this:

[private] inherit [prefix] "<file path>";
e.g.
        inherit "/std/door";
        inherit "/usr/common/object";
        private inherit foo "/usr/bob/secret_parent";
    

Note that this is not a preprocessor command, it is a statement, so it does not have a # in front of it. It also ends with a ;. You may specify that it's a .c file if you wish, but doing so isn't required.

Inheritance statements must come before any variable or function definitions, including in #include files. This is one reason you can't use the standard include file (mentioned in a later chapter) to add a variable to every LPC program -- if you did, that variable would be declared before any inheritance, so no LPC program could inherit anything!

The child object (the one that declares the inheritance, as above) will inherit all inheritable functions and variables. This means that simply calling a function with the name declared in the parent will call that function as the parent defines it. Or, if the child defines it, it will be called with the child's definition. That is the power of inheritance -- the same name can refer to any of a family of functions, tailored to different classes.

Variables are also inherited, and can be referred to by name. The variable's name, by itself, points to the parent's instance of that variable, so it works just like functions.

If a child object has a function with the same name as a function in the parent, the child's function will mask the parent's. When the function is called by an external call to the child, the child function will be executed. To call the parent function from the child, call the function name with the scope operator, ::, before it.

void my_func()
{
    /* 
     * This function exists in the parent, and I need to
     * call it from here.
     */
    ::my_func();        /* Call my_func() in the parent. */
}
    

If a parent is inherited with a prefix, for example, inherit foo "/usr/bob/fooclass", the method above won't work. Instead of calling with a scope operator before it, it must be called with the prefix, the scope operator and then the function name. So ::my_func(); above might become foo::myfunc();.

It is not possible to call a masked function in the parent by an external call -- only from within the object itself. If an object inherits an object that has inherited another object, e.g. C inherits B that inherits A, then masked functions in A are available in B. If B masks that function then C will get B's version when it calls the function with the scope operator. If B doesn't mask the function then C would get A's version instead.

If a parent is inherited with the private keyword, only the class inheriting it will be able to see its functions. External function calls won't find the private parent's functions, and child classes won't be able to call the functions inherited from that parent. To export the functions in a private parent class, have the child class declare functions with the same names that pass the arguments through to the parent.

Type identification

Due to the fact that all variables are initialized to 0 or nil, and that many functions return 0 or nil when failing, it's desirable to be able to determine what value you actually have received. If you use the mixed type it's even more essential to be able to test what the mixed variable contains at any specific time. For this purpose there's a special function called typeof(). It uses constants listed in the header file <type.h>.

Calling typeof() on a value returns one of the constants from type.h, though you may get a type different from the one the variable has. For instance, an uninitialized string, array or mapping will return T_NIL rather than T_STRING, T_ARRAY or T_MAPPING. This is true even if typeof() is called on a variable of type string rather than type mixed.

One excellent use for typeof() is to write a function which allows multiple possible types for a single parameter and checks the type of that parameter inside. For instance:

#include <type.h>

string mixed_print_to_string(mixed arg)
{
    switch(typeof(arg))
    {
        case T_NIL:
            return "(nil)";
        case T_STRING:
            return "\"" + arg + "\"";
        case T_INT:
        case T_FLOAT:
            return "" + arg;
        case T_OBJECT:
            return "<" + object_name(arg) + ">";
        case T_ARRAY:
            {
                int    ctr;
                string tmp;

                tmp = "({ ";
                for(ctr = 0; ctr < sizeof(arg) - 1; ctr++)
                {
                    tmp += mixed_print_to_string(arg[ctr]);
                    tmp += ", ";
                }
                /* We go one less iteration and then add the final
                   element by hand to avoid a stray comma at the
                   end. */
                tmp += mixed_print_to_string(arg[sizeof(arg) - 1]);
                return tmp;
            }
        default:
            error("We don't print those yet!");
    }
}
    

Type qualifiers

The types you assign to variables and functions can have qualifiers changing the way they work. It's very important to keep these qualifiers in mind and use the proper ones at the proper times. Most work differently when applied to variables rather than functions, so try to avoid confusion by remembering that, for instance, a static variable and a static function have little, if anything, to do with each other.

The static variable qualifier

Static variables must be global variables. Global variables are variables defined outside of any function in the file. These variables are visible and usable in all functions defined later in the file than they are, so their scope is object-wide rather than limited to one function.

It is possible to save the global variables of an object with a function called save_object and restore them with restore_object. If a global variable is declared as static, it is not saved or restored along with the others. DGD doesn't particularly recommend using save_object anyway -- it's better to use statedumps or some other form of structured storage in most cases.

static string   TempName;  /* An example of a non-saved global variable. */
    

The static function qualifier

A function that is declared static can not be called using external calls (call_other), only internal. This makes the function invisible and inaccessable to other objects. Child classes of the object may call the object's static functions, so be sure to take that into account. Functions that check the caller with previous_program() may be better for security purposes. You can also combine the two approaches.

Since you can't call a static function with call_other() from another object, you also can't call it with object->func() syntax, since that's equivalent to call_other(). If the static function is later masked (see the 'nomask' function modifier for details on masking), the call won't be redirected as usual.

Static functions defined by the AUTO object (see chapter 5) are special. They are treated just like built-in kernel functions -- they can't be called with call_other(), for instance.

The private function and variable qualifier

A variable or function that has been declared private will not be usable by other objects, including child objects. Private functions and variables can only be accessed by the object that defines them. Private variables, like static variables, are not saved by save_object().

The nomask function qualifier

Functions that are declared as nomask can not be masked by inheritance. That is to say, a child object may not declare another function with the same name. Normally a function with the same name in a child object would simply replace the original function silently (called masking it). A function marked nomask may not be replaced in this way. Attempting to do so gives an error message.

Since an internal call will still use the child class's method definition by default, nomask functions provide a way to be sure what method is being called. Functions that are declared as both static and nomask are a powerful way to make sure a function can be called only by child classes, but the definition of the function will always be known for any child class. A private function may not also be declared nomask.

Variables may not be marked nomask -- they have the same functionality automatically, so there is no use for a nomask variable modifier.

The atomic function qualifier

Atomic functions are very powerful, and are specific to DGD Only functions may be declared atomic, not variables. The atomic modifier is used just like the static or nomask modifier, before a function's name.

If a function is declared atomic, it may not read, write, move or rename files in any way. It may not read or write network data on network connections. Any attempt to do so will cause an error. Atomic functions also use a different error handler than non-atomic functions. They use up twice as many ticks per operation as non-atomic functions (see rlimits for an explanation of ticks).

So what's different about them? It's the fact that they will either execute completely and without error, or nothing will happen. You can call other functions or write to global variables and data structures in an atomic function, even if those variables or data structures are in a different object than the atomic function. If an error occurs and the function terminates, those writes to variables and data structures will be fully undone, as will the calls to other objects. It is as though the atomic function has never been called at all, except for the fact that it causes an error to be handled.

Calls to atomic functions may be nested. That is to say, atomic functions may call other atomic functions, catching errors if necessary. They may also, of course, call non-atomic functions. Changes will only be undone when an error forces execution to leave the atomic function; an error caught within an atomic function will not be undone. If an error is caught in code that called the atomic function, changes will be undone before the error is caught.

Atomic functions are one of the most powerful features of DGD. They are found in no other commonly-used language, although many databases have a very similar feature. Several languagues like Prolog have backtracking, which can be used for similar purposes, but that's often much less convenient than atomic functions for standard MUD operations.

switch/case part 2

The LPC switch statement is very intelligent, it can also use ranges in integers:

void wheel_of_fortune()
{
    int i;

    i = random(10);     /* Get a random number from 0 to 9 */

    switch (i)
    {
    case 0..4:
        write("Try again, sucker!\n");
        break;

    case 5..6:
        write("Congrats, third prize!\n");
        break;

    case 7..8:
        write("Yes! Second prize!\n");
        break;

    case 9:
        write("WOOOOPS! You did it!\n");
        break;

    default:
        write("Someone has tinkered with the wheel... Call 911!\n");
        break;
    }
}
    

catch/error: Error handling at runtime

It happens now and then that you need to make function calls you know might result in a runtime error. For example you might try to clone an object (described later) or read a file. If the files aren't there or your privileges are wrong you will get a runtime error and execution will stop. In these circumstances it is desirable to intercept the error and either alert a user or try alternate solutions to the problem. The special LPC function operator catch() will do this for you. If an error occurs during evaluation of the given function, the error is returned.

int catch(function)
e.g.
    if (catch(write_file("/usr/bob/logfile", "It works!")))
    {
        DRIVER->message("You don't have permission to write!\n");
        return;
    }
    

It's also possible to cause errors. This is particularly useful when you want to notify the user of an unplanned event that occured during execution. For instance, you often want to do this in the 'default' case of a switch statement. In any case, error() will generate a runtime error with the message you specify. A catch() statement issued prior to calling the function that uses throw() will intercept the error as usual.

error(string message)
e.g.
    if (test < 5)
        error("The variable 'test' is less than 5\n");
    

Array and Mapping references

In computer science terms, arrays and mappings are copied by reference and simpler types are copied by value This means that arrays and mappings, unlike other variables, aren't copied every time they are moved around. Instead, what is moved is a reference to the original array or mapping. What does this mean then?

Here's an example:

object *arr, *copy_arr;

arr = ({ 1, 2, 3, 4 });    /* An array */

copy_arr = arr;              /* Assume (wrongly) that a copy_arr becomes
                                a copy of arr. */

/* Change the first value (1) into 5. */
copy_arr[0] = 5;
    

It might be logical to assume that the first value of copy_arr is 5 while the first value or arr is 1. That's not the case, because what got copied into copy_arr was not the array itself, but a reference to the same array as arr. This means that assigning an element changed that element in the original array to which both variables refer. copy_arr and arr will both seem to have changed, while in fact it was only the original array that changed.

Exactly the same thing will happen for mappings since they are also copied by reference.

How do you copy an array or mapping by value? Usually you want to work on a copy and not the original array or mapping.

              _ This is just an empty array
             /
copy_arr = ({ }) + arr;
                    \_ This is the one we want to make unique
    

In this example copy_arr becomes the sum of the empty array and the arr array created as an entirely new array. This leaves the original unchanged, just as we wanted. You can do exactly the same thing with mappings. It doesn't matter if you add the empty array or mapping first or last, just as long as you do it.

You can also copy arrays using array-slice notation. This is probably the most common in existing DGD code.

copy_arr = arr[..];
    

At the end of a thread of execution, any array that is in a new object will be copied into that object. That means you can't just store references to a single array in a lot of your objects and use those references to share data. At the end of their first thead of execution, each referenced mapping and array will be copied into all the objects that reference them, and they'll stop being the same as the original. To share data like that, you should put the array in a single object and make a function that returns the array. Then any object that wants to modify it can call the function, modify the array and then stop referencing it before the thread of execution is over. You could also have separate "get" and "set" functions, which would be slower but easier to control.

For details on what threads of execution are and when they end, see section 5.3.1 in chapter 5.


SourceForge.net Logo
Noah Gibbs
Last modified: Tue Jul 1 15:01:01 PDT 2003