This document is intended to help you to get started with coding in LPC more quickly. Many people who become wizard have never programmed before, and even if they have experience in that field, then usually with a language that differs substantially from LPC. In this document we will explain the syntax of LPC, and some other things that are usefull to know.
It could very well be that you miss certain parts in this document, or that you feel that important concepts are not explained where they should have. I urge you to let us know what you miss, and what changes you would like to see made. Most of the administrators are veteran coders, and to speak for myself, it is hard to picture what level of explanation is deep enough for a beginning user. To me, things might seem obvious, when they are in fact not at all that trivial. Mail an Arch, or "sysbug" it, and we will try to keep this document as helpfull as possible. Your help is greatly appreciated.
Thanks must go to Nick, who sparked the flame of my activity by putting together a compilation of the old LPC files, and who I am sure will try hard to keep this file as correct as possible.
As we all know, the LPMud world (named after the initiator: Lars Pensj|'s Multi user dungeon) consists of many objects. We can pick them up, do things with them, we can walk around in them, heck, even we ourselves are objects in the world. The world is really a large quantity of little programs that are maintained by a big interpreter, or compiler as you wish, that runs on a computer somewhere. This interpreter is what people call "the gamedriver".
At startup, the gamedriver loads some initial objects, and then sets up a connection, waiting for people to log in. As people log in and walk around, they will encounter objects that have not been loaded yet. The gamedriver quickly loads them just before they get to see them, so it will look just as if the world always has been there. Sometimes this goes wrong; then you will get to see the famous "wrongness in the fabric of space" message.
All the code that the driver can load is called the mudlib. The mudlib are the routines that allow the world to be built. The things that you inherit, most of the functions you use, they comprise the mudlib.
The programs that the gamedriver tries to load are written in the language called LPC. The C at the end is no coincidence. LPC is closely related to the language C. For those of you who start to cringe in terror: relax, it is less complicated than C. The syntax looks very much like the syntax of C. If you want better explanation about the syntax than that shown below, you might want to take a peek at Kernighan and Ritchie's "Programming in C", which is a very clear book on the language C.
First of all, you will want to write an object. You can write an object using the editor "ed", or you can use your favourite editor at home, and ftp your object to the site to which you usually telnet, using port 3001 Use your own login name and your own password. Once you are in, you can "put" the file in your own directory, or "get" one from it.
The ftp method seems to be more work, but to many people the use of their favourite editor outweighs those disadvantages. However, I also know people who wouldn't trade in "ed" for the world. Ah well, take your pick.
Once you have written your object, you can try to "load <filename>" from within the mud. You will probably get an error message, which you can examine with "errlog", or with "tail /lplog". The latter command shows all errors the gamedriver encountered, and if you are quick enough, your error will be on the bottom of what you get to see.
Once the object is bugfree, you can try to "clone <filename>". Cloning will give you a copy of the object that you have written. You can clone something more than once, resulting in multiple objects that are all alike. Cloning is exactly the same a loading, except that loading will not give you an object.
If you want to get rid of some of those copies, you can "destruct" them.
If you have loaded or cloned an object, the gamedriver will keep the code of that object in memory, so it doesn't have to go through the trouble of compiling it again next time you want to clone it. Consequently, if you have cloned something and you have changed the file, a renewed clone will still give you a copy of the same old object as before. The gamedriver should first be notified that it has to forget about the old code. To do this, there is the command "update <filename>". So if you first update the file, and then clone it, you will get the new and improved object.
You will find no examples of code in this document, you can find them in the INTRO doc. There you will also find a description of possible bugs you might encounter on your trip through LPC-country.
Before we take a look at the language, let's mention that the gamedriver does not care how you write your code. You use tabs, spaces, newlines wherever you see fit to decorate your code. There are some exceptions, like strings. Strings always have two quotes '"', and those two quotes have to be on the same line. Of course you can use the linebreak character '\' to glue a few lines together, but that is the same as telling the gamedriver that those lines are in fact one line.
However, often other people also have to work with the code you have written. Readability of your code is greatly improved if you use the standard indentation rules, as they are followed in this document. The also is a command that will indent code for you, it is conveniently called "indent <filename>".
Enough beating around the bush. Let us get down with what we promised; describing the language. Like most languages, LPC consists of three main things:
Note that LPC is case sensitive; the functions foo_bar() and FoO_BaR() have different names.
LPC knows very few types, that is what keeps it from being complex. Here are all types LPC knows:
From the above types, except for void, one can construct arrays as well by adding a '*' before the variable name.
All uninitialized variables will have the value 0. A pointer to a destructed object will also be 0.
Global variables can have a certain qualifier. 'static' Will prohibit the variable from being saved with save_object() or being destroyed by restore_object(). A global variable can also be 'private' which means that is is only addressable from within the inheritance block in which it is defined.
Examples:
string name_str;
int a, b, c;
int *num_arr, *fib_arr = ({ 1, 1, 3, 5 }), x = 3;
static object owner;
private int foo = 12345;
mapping Q = ([ "f":"foo", "b":"bar" ]);
About the used notation:
If you ever need truthfunctional values, LPC considers 0 to be false, and everything other than 0 is considered to be true. This goes for all types. If the variable is empty, it is considered to be not 0, and thus true. So the string "" is true, just like the array ({ }) and the mapping ([ ]).
These are the operators availailable in LPC. They are listed in the order or precedence (low priority first):
Statements in LPC have to be separated from each other with semicolons. Sometimes it is necessary to group a lot of statements together; in that case you construct a block.
A block is a special statement, that begins with '{', contains a list of statements, and ends with '}'. Variables defined inside the block are local to that block, and override variables with the same name from outside the block. The variables have to be defined at the beginning of the block, before every other statement of the block.
A locally defined function can have any number of arguments. All basic types can be given as an argument. Unitialized arguments are set to 0.
As return value, functions can use the same types as variables. They can also have qualifiers to specify the scope of the function. A return value is sent with the 'return' statement, after which the execution of the function will also return to its caller. All data types except void can be used in the return statement. If the function is of the type 'void', simply "return;" is enough to return.
It is not mandatory to declare the type of a function, or the types of the arguments. If you use types, the gamedriver will be able to give more accurate error messages. However, the gamedriver will also be a lot pickier, and will complain if you use functions before you declare them. Let us suppose that on line 42 in our code, there is a call to the function "foo()". If the function is defined on line 57, the gamedriver will complain, because it does not know yet what the function looks like.
There are two ways to solve this:
It is illegal to have a function with the same name as a global function, or local variable. If there is no return statement, 0 will be returned.
These qualifiers are applicable to functions:
The 'varargs' qualifier may be used together with any other qualifier.
This is what a function looks like:
<qualifier> <type>
function_name(argument1, argument2 ...)
{
statements;
...
return value;
}
Note that the arguments that are received are copies of the originals that were supplied. You may assign new values to them if you like, but the calling function will not notice anything. This is not true for arrays, but fiddling around with the arguments is considered bad coding practice. If you want to return a changed array, simply return it and don't return nothing except a sneakily changed argument.
Functions in other objects can be called with 'obj->fun(arg, ...)'. Another way to get the same effect is 'call_other(ob, "fun", arg...)'. The first method looks more natural. Static functions cannot be called in other objects.
An object can inherit all variables and functions from another object. This will save you from having to redefine the functions defined in the other object. You inherit other objects with the declaration 'inherit "file";'. This must come before any local variables or functions of this object. You may inherit more than one objects if you want to.
Functions of the inherited file can be called as if they were defined like other functions in the same object, unless they were qualified as 'private'.
Functions of the inherited object may be redefined, as long as they are not 'nomask'. It might occur that you still want to call the original of the redefined function, then you can call it by appending "::" in front of the name.
Here is a simple example, where you should keep in mind that the all the called functions are defined in the inherited object:
inherit "/std/room";
void
create_room()
{
set_name("Workroom");
set_long("This is the workroom of someone.\n");
add_exit("/d/Genesis/wiz/flame", "flame");
}
void
init()
{
::init();
write("The workroom greets you.\n");
}
There is support for arrays. An array can be declared by putting a '*' in front of the variable definition, like:
The array constructors are '({' and '})'. Elements of an array are separated by commas and arrays can be added to or subtracted from each other. Elements can be of any type, even other arrays.
You can build arrays directly by assigning them to an array-definition, or indirectly by adding array-definitions to them. Assigning values to indexes that are not in the range [0, sizeof(array)-1] will lead to errors. Another way of building arrays is to allocate() them. In that case you will get an empty array of the allocated size, to which you can start to assign (within the size limits, that is).
Examples:
int *arr, *arr2;
arr = ({ 1, 2, 6, 10 });
arr += ({ 11 });
arr2 = allocate(3);
arr2[0] = 4;
Arrays are stored by reference, so all assignments of whole arrays will just copy the address. The array will be deallocated when no variable points to it any longer.
When a variable points to an array, items can be accessed with indexing: 'arr[3]' as an example, will give the fourth element of the array 'arr'. Note that the numbering starts from 0. The 'sizeof()' function will return the size of the array. That means that there is no element numbered sizeof(arr) in an array, all elements have lower numbers.
The name of the array being indexed can be any expression, even a function call: 'func()[2]'. It can also be another array, if this array has pointers to arrays.
Now 'arr[1][2]' is a valid value, namely 12.
Elements of arrays can be treated just like normal variables, e.g.:
Then
There are some special statements that allow selection and repeating depending on testing conditions.
if (expr) statement1; else statement2;
The if-statement can be used to direct the flow of execution according to the value of 'expr'. If 'expr' is true, 'statement1' is chosen, if it was false 'statement2' will be executed. If you want more than one statement to be executed in either the if- or the else-part, you should enclose all statements in a block.
for (expr1; expr2; expr3) statement;
The 'for' statement is used to create loops. The actual body of the loop is 'statement'. If you want more than one statements to be looped, you should enclose them in a block.
This is what happens: initially 'expr1' is once executed. Then, while 'expr2' yields true, 'statement' is executed. Every time 'statement' has been executed, or a 'continue' statement has been executed, execute 'expr3' before doing the next loop. The three arguments of 'for' may each be empty, in which case they are presumed to return true.
A 'break' in the 'statement' will terminate the loop. A 'continue' will continue the execution from the beginning of the loop, without executing the rest of the statement. Putting 'return' in the statement will leave the function, and therefore also the for-loop.
Example:
for (i = 0; i < 10; i++)
{
write(i + "\n");
foo += i;
}
for (;;i++)
{
if (i == 15)
break; /* Stop at 13 */
if (i % 2 == 0)
continue; /* Don't print even numbers */
write(i + "\n");
}
while (expr) statement;
While 'expr' is true, execute statement. If 'statement' is more than one statement, it should be a block.
A 'break' in the 'statement' will terminate the loop. A 'continue' will continue the execution from the beginning of the loop, not executing the statements that are still left.
Example:
while (i < 11)
{
i += 3;
if (i % 2 == 0)
continue; /* Don't print the even numbers */
write(i + "\n");
}
switch (expr)
{
case val1;
statement1;
...
default;
statementd;
}
The 'switch'-statement is like a multiple 'if'-statement. It considers a value, and picks the all cases that match to execute, starting from the top. The default case always matches, but you are not required to define it. If 'statement1' consists of multiple statements, you are not required to make a block of it.
Beware! If you only want to execute the statement of one of the cases, make sure you put "break;" before the next case. If you forget to do that, every matching case will be executed. Usually you will want to use "break;".
Example:
switch (i)
{
case 1:
write("I was 1.\n");
break;
case 2:
case 3:
write("I was 2 or 3.\n");
break;
default:
write("I was not 1, 2 or 3.\n");
break;
}
The 'switch'-statement will work on strings as well.
Because people tend to forget how a function exactly works, it might be usefull to put a little word of explanation in your code to freshen up the memory. This can be done by putting comments in your file. A comment is opened with the sequence '/*', and closed with the first encountered '*/'. Note that this rule provides you from putting comments inside comments.
In the standard mudlib standard headers are used to explain what all functions are about, perhaps it is a good idea to adopt such headers.
Here is an example of such a header:
/*
* Function name: foobar
* Description: This function computes how much rain would
* mainly fall in the plains in Spain.
* Arguments: arm: The number of armadillo's
* zuc: The zucchini-count
* Returns: 0 on error; else the amount of rain.
*/
varargs static int
foobar(int arm, int zuc)
{
....
}
The preprocessor is really a kind of meta-compiler. Before the gamedriver tries to examine the code, the preprocessor skips through it, to see if there were any special commands for it, and it changes the code accordingly. After that the gamedriver will try to compile the code that remains. Like the normal compiler, the preprocessor ignores comments.
#include "/path/file" or #include <file>
The '#include'-statement will include the text of the specified file in the current file. The result is the same as what you would get if you typed the whole file on the same spot as the '#include'.
There are two ways to specify the file to include. Either the file is in quotes, like '"/sys/formulas.h"', in which case the preprocessor will use that as a path. Or the file is in brackets, like '<formulas.h>', in which case the preprocessor will start to search through the includedirectories that are known to it.
#define ARMADILLO #define FOO 2 + 2 #define SETNAME(pers,str,val) if (val * 3 > 5) \ pers->set_name(str); #undef ZUCCHINI
This statement allows you to define constants, or to set them to a value. The '#define'-statement causes all occurrences of the defined first part to be textually replaced with the second part. It is possible to define functions that use variables, where the arguments given are textually copied to the places where they are used in the function. The resulting text is copied to where the function-call was made.
If no value is assigned to the defined object, is it defined to true. Things that are not defined are false. To make sure that a certain constant is not defined, you can '#undef' it.
Beware! It is not for nothing that I stress that things are textually replaced. Look at the former defines. What would the next things give?
write("FOO * 5 = " + FOO * 5 + "\n");
SETNAME(obj, "Fatty", 1+1);
Don't be mislead by the interpretation of FOO. "2 + 2" Might mean "4", but when multiplied by 5 behaved differently! "2 + 2 * 5" is evaluated as 2 + (2 * 5) which is not the same as (2 + 2) * 5!
The same thing happens with SETNAME: 'val' is replaced with '1+1', which gives '1+1 * 3' and is evaluated as 1 + (1 * 3), and not as the intended (1 + 1) * 3!
It is safest to surround the define with braces, or to enclose the constant names in braces.
Note that all constantnames that are used for the preprocessor are in capitals. That is just a convention which makes it easier to keep normal variables and defined variables apart.
#if DEBUG == 1 ... #else ... #endif #ifdef ARMADILLO ... #else ... #endif #ifndef ZUCCHINI ... #else ... #endif
The '#if'-, '#ifdef'- and '#ifndef'-statements allow you to selectively include code. Everything that is in the right 'if'-branch is compiled, the rest not. Behind the '#if'-statement may be an expression with preprocessor-constants.
'#ifdef' checks to see if the constant behind it is defined or not, '#ifndef' checks if it is not defined.