Phantasmal MUD Lib for DGD
|
Phantasmal Site > DGD > Untitled document (index.base.html) > (untitled) Essential LPCThis 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 comeTo present things on the screen for the player to read, the
There are two special characters that are often used to format
text, tab and newline. They are written as
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 revisitedLet'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 callsThere 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 <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
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 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 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-referenceIn 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 classesAssume 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 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, 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 identificationDue 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 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 qualifiersThe 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 qualifierStatic 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 static string TempName; /* An example of a non-saved global variable. */ The static function qualifierA function that is declared 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 qualifierA variable or function that has been declared
The nomask function qualifierFunctions that are declared as Since an internal call will still use the child class's method
definition by default, Variables may not be marked The atomic function qualifierAtomic functions are very powerful, and are specific to DGD Only
functions may be declared atomic, not variables. The
If a function is declared 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 2The 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 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(string message) e.g. if (test < 5) error("The variable 'test' is less than 5\n"); Array and Mapping referencesIn 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
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 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. Noah Gibbs Last modified: Tue Jul 1 15:01:01 PDT 2003 |