LPC Basics
Written by Descartes of Borg
23 april 1993
This manual, how to use it, and its terms
I have seen a lot of requests lately on USENET for LPC manuals. In addition, the immortals on my mud have been telling how good the building documentation of Nightmare is, but that there was just no adequate explanation of the LPC programming language. So I decided to try my hand at writing a manual.
Some things you should keep in mind.
LPC is a very easy programming language to learn, and it has real
value in that place most of us know as the real world. I began playing
muds in 1991, and in the space of a month created an unimpressive area
and musician's guild on the original Bates College MUD called Orlith.
After that, I moved to Los Angeles for a year and had no contact with
mudding or computers. In June of 1992, I was back on the internet and
a wizard of Igor. In September of 1992 I began coding the Nightmare
mudlib for our use, and then later decided to distribute it due to there
not being any mudlibs for MudOS at the time that a person could just throw
up a running mud with (now, that of course is not the case :)).
So, I have been doing serious coding for less than a year. As a
Philosophy major in a world of Computer Science majors, I just want to
make clear that it is not at all required that you have ever done anything
with your computer than log into a mud in order for you to really come
to understand LPC coding. This manual makes the following assumptions:
The chapters of this manual are meant to be read in order. Starting with the introduction, going sequentially through the chapter numbers as ordered in the contents file. Each chapter begins with a paragraph or two explaining what you should have come to understand by that point in your studies. After those introductory paragraphs, the chapter then begins to discuss its subject matter in nauseating detail. At the end of the chapter is a briefly worded summary of what you should understand from that chapter if I have been successful. Following that may or may not be some sidenotes relevant to the subject at hand, but not necessary to its understanding.
If at any time you get to a chapter intro, and you have read the preceeding chapters thoroughly and you do not understand what it says you should understand by that point, please mail me! Clearly, I have failed at that point and I need to know where it is I have gone wrong so I can revise it properly. Similarly, if you do not understand what the chapter summary says you should, please mail me. If your mud is on the MudOS intermud system, mail descartes@nightmare. Otherwise mail borg@hebron.connected.com.
Some basic terms this manual uses:
George Reese
(Descartes of Borg)
12 july 1993
borg@hebron.connected.com
LPMuds use basic UNIX commands and its file structure. If you know UNIX commands already, then note (with a few exceptions) options are not available to the commands. Like DOS, UNIX is hierarchical. The root directory of which all directories are sub-directories is called root(/). And from those sub-directories you may have further sub-directories. A directory may be referred to in two different ways: 1) by its full name, or absolute name, or 2) by its relative name. Absolute name refers to the directory's full path, starting from / winding down the directory tree until you name the directory in question. For example:
/players/descartes/obj/monster
refers to the directory monster which is a sub-directory of obj which is a sub-directory of descartes which is a sub-directory of players which is a sudirectory of /.
The relative name refers to the name relative to another directory. The above example is called monster relative to /players/descartes/obj, but it is also called obj/monster relative to /players/descartes, descartes/obj/monster relative to /players, and finally players/descartes/obj/monster relative to /. You can tell the difference between absolute names and relative names because absolute names always start with /. In order to know exactly which directory is being named by a relative name, you naturally must know what directory it is relative to.
A directory contains sub-directories and files. LPMuds only use text files inside the mudlib. Like directories, files have both absolute and relative names. The most basic relative name is often referred to as the file name, with the rest of the absolute name being referred to as the path. So, for the file: /players/descartes/castle.c, castle.c is the file name, and /players/descartes is the path.
On some muds, a file with a file name beginning with a . (like .plan) is not visible when you list files with the regular file listing command.
Along with the UNIX file structure, LPMuds use many UNIX commands. Typical UNIX commands on most muds are:
pwd, cd, ls, rm, mv, cp, mkdir, rmdir, more, head, cat, ed
If you have never before seen UNIX commands, you probably are thinking this is all nonsense. Well, it is, but you have to use them. Before getting into what they mean though, first a discussion of current directory. If you know DOS, then you know what a current working directory is. At any given point, you are considered to be "in" some directory. This means that any relative file or directory names you give in UNIX commands are relative to that directory. For example, if my current directory is /players/descartes and I type "ed castle.c" (ed is the command to edit), then it assumes I mean the file /players/descartes/castle.c
UNIX uses a hierarchical file structure with the root of the tree being named /. Other directories branch off from that root directory and in turn have their own sub-directories. All directories may contain directories and files. Directories and files are referred to either by their absolute name, which always begins with /, or by their relative name which gives the file's name relative to a particular directory. In order to get around in the UNIX files structure, you have the typical UNIX commands for listing files, your current directory, etc. On your mud, all of the above commands should have detailed help commands to help you explore exactly what they do. In addition, there should be a very detailed file on your mud's editor. If you are unfamiliar with ed, you should go over this convoluted file.
The title of this chapter of the textbook is actually poorly named, since one does not write programs in LPC. An LPC coder instead writes *objects*. What is the difference? Well, for our purposes now, the difference is in the way the file is executed. When you "run" a program, execution begins at a definite place in the program. In other words, there is a place in all programs that is noted as the beginning where program execution starts. In addition, programs have definite end points, so that when execution reaches that point, the execution of the program terminates. So, in short, execution of a program runs from a definite beginning point through to a definite end point. This is not so with LPC objects.
With muds, LPC objects are simply distinct parts of the C program which is running the game (the driver). In other words, execution of the mud program begins and ends in the driver. But the driver in fact does very little in the way of creating the world you know when you play a mud. Instead, the driver relies heavily on the code created in LPC, executing lines of the objects in the mud as needed. LPC objects thus have no place that is necessarily the beginning point, nor do they have a definite ending point.
Like other programming languages, an LPC "program" may be made up of one or more files. For an LPC object to get executed, it simple needs to be loaded into the driver's memory. The driver will call lines from the object as it needs according to a structure which will be defined throughout this textbook. The important thing you need to understand at this point is that there is no "beginning" to an LPC object in terms of execution, and there is no "end".
As I have mentioned earlier, the driver is the C program that runs on the host machine. It connects you into the game and processes LPC code.
Note that this is one theory of mud programming, and not necessarily better than others. It could be that the entire game is written in C. Such a game would be much faster, but it would be less flexible in that wizards could not add things to the game while it was running. This is the theory behind DikuMUDs. Instead, LPMUDs run on the theory that the driver should in no define the nature of the game, that the nature of the game is to be decided by the individuals involved, and that you should be able to add to the game *as it is being played*. This is why LPMUDs make use of the LPC programming language. It allows you to define the nature of the game in LPC for the driver to read and execute as needed. It is also a much simpler language to understand than C, thus making the process of world creation open to a greater number of people.
Once you have written a file in LPC (assuming it is corrent LPC ), it just sits there on the host machine's hard drive until something in the game makes a reference to it. When something in the game finally does make a reference to the object, a copy of the file is loaded into memory and a special *function* of that object is called in order to initialize the values of the variables in the object. Now, do not be concerned if that last sentence went right over your head, since someone brand new to programming would not know what the hell a function or a variable is. The important thing to understand right now is that a copy of the object file is taken by the driver from the machine's hard drive and stored into memory (since it is a copy, multiple versions of that object may exist). You will later understand what a function is, what a variable is, and exactly how it is something in the game made reference to your object.
Although there is no particular place in an object code that must exist
in order for the driver to begin executing it, there is a place for which
the driver will search in order to initialize the object. On compat
drivers, it is the function called reset(). On native muds it is the
function called create().
<* On Kingdoms it is called create_object() - Ceril *>
LPC objects are made up of variables (values which can change) and functions which arefunctions which are used to manipulate those variables. Functions manipulate variables through the use of LPC grammatical structures, include calling other functions, using externally defined which functions (efuns), and basic LPC expressions and flow control mechanisms.
Does that sound convoluted? First lets start with a variable. A variable might be something like: level. It can "vary" from sitation to situation in value, and different things use the value of the player's level to make different things happen. For instance, if you are a level 19 player, the value of the variable level will be 19. Now if your mud is on the old LPMud 2.4.5 system where levels 1-19 are players and 20+ are wizards, things can ask for your level value to see if you can perform wizard type actions. Basically, each object in LPC is a pile of variables with values which change over time Things happen to these objects based on what values its variables hold. Often, then things that happen cause the variables to change.
So, whenever an object in LPC is referenced by another object currently in memory, the driver searches to see what places for values the object has (but they have no values yet). Once that is done, the driver calls a function in the object called reset() or create() (depending on your driver) which will set up the starting values for the object's variables. It is thus through *calls* to *functions* that variable values get manipulated.
But create() or reset() is NOT the starting place of LPC code, although it is where most LPC code execution does begin. The fact is, those functions need not exist. If your object does just fine with its starting values all being NULL pointers (meaning, for our purposes here, 0), then you do not need a create() or reset() function. Thus the first bit of execution of the object's code may begin somewhere completely different.
Now we get to what this chapter is all about. The question: What consists a complete LPC object? Well, an LPC object is simply one or more functions grouped together manipulating 0 or more variables. The order in which functions are placed in an object relative to one another is irrelevant. In other words:
-----void init() { add_action("smile", "smile"); } void create() { return; } int smile(string str) { return 0; }----- is exactly the same as: -----
void create() { return; } int smile(string str) { return 0; } void init() { add_action("smile", "smile"); }----- Also important to note, the object containing only: -----
void nonsense() {}-----
is a valid, but trivial object, although it probably would not interact properly with other objects on your mud since such an object has no weight, is invisible, etc..
LPC code has no beginning point or ending point, since LPC code is used to create objects to be used by the driver program rather than create individual programs. LPC objects consist of one or more functions whose order in the code is irrelevant, as well as of zero or more variables whose values are manipulated inside those functions. LPC objects simply sit on the host machine's hard driver until referenced by another object in the game (in other words, they do not really exist). Once the object is referenced, it is loaded into the machine's memory with empty values for the variables. The function reset() in compat muds or create() in native muds is called in that object if it exists to allow the variables to take on initial values. Other functions in the object are used by the driver and other objects in the game to allow interaction among objects and the manipulation of the LPC variables.
A note on reset() and create():
create() is only used by muds in native mode (see the textbook Introduction for more information on native mode vs. compat mode). It is only used to initialize newly referenced objects.
reset() is used by both muds in compat mode and native mode. In compat mode, reset() performs two functions. First, it is used to initialize newly referenced objects. In addition, however, compat mode muds use reset() to "reset" the object. In other words, return it to its initial state of affairs. This allows monsters to regenerate in a room and doors to start back in the shut position, etc.. Native mode muds use reset() to perform the second function (as its name implies).
So there are two important things which happen in LP style muds which cause the driver to make calls to functions in objects. The first is the creation of the object. At this time, the driver calls a function to initalize the values in the object. For compat mode muds, this is performed by the function named reset() (with an argument of 0, more on this later though). For muds running in native mode, this is performed by the function create().
The second is the returning of the room to some base state of affairs. This base set of affairs may or may not be different from the initial state of affairs, and certainly you would not want to take up time doing redundant things (like resetting variables that never change). Compat mode muds nevertheless use the same function that was used to create the object to reset it, that being reset(). Native mode muds, who use create() to create the room, instead use reset() to reset it. All is not lost in compat mode though, as there is a way to tell the difference between creation and resetting. For reset purposes, the driver passes either 1 or the reset number as an argument to reset() in compat mode. Now this is meaningless to you now, but just keep in mind that you can in fact tell the difference in compat mode. Also keep in mind that the argment in the creation use of reset is 0 and the argument in the reset use is a nonzero number.
LPC object are made up of zero or more variables manipulated by one or more functions. The order in which these functions appear in code is irrelevant. The driver uses the LPC code you write by loading copies of it into memory whenever it is first referenced and additional copies through cloning. When each object is loaded into memory, all the variables initially point to no value. The reset() function in compat muds, and create() in native muds are used to give initial values to variables in objects. The function for creation is called immediately after the object is loaded into memory. However, if you are reading this textbook with no prior programming experience, you may not know what a function is or how it gets called. And even if you have programming experience, you may be wondering how the process of functions calling each other gets started in newly created objects. Before any of these questions get answered, however, you need to know more about what it is the functions are manipulating. You therefore should thouroughly come to know the concept behind LPC data types. Certainly the most boring subject in this manual, yet it is the most crucial, as 90% of all errors (excepting misplaced {} and ()) involve the improper usage of LPC data types. So bear through this important chapter, because it is my feeling that understanding this chapter alone can help you find coding much, much easier.
You possibly already know that computers cannot understand the letters and numbers used by humans. Instead, the "language" spoken by computers consists of an "alphabet" of 0's and 1's. Certainly you know computers do not understand natural human languages. But in fact, they do not understand the computer languages we write for them either. Computer languages like BASIC, C, C++, Pascal, etc. are all intermediate languages. They allow you to structure your thoughts more coherently for translation into the 0's and 1's of the computer's languages.
There are two methods in which translation is done: compilation and interpretation. These simply are differences betweem when the programming language is translated into computer language. With compiled languages, the programmer writes the code then uses a program called a compiler to translate the program into the computer's language. This translation occurs before the program is run. With interpreted languages however, the process of translation occurs as the program is being run. Since the translation of the program is occurring during the time of the program's running in interpreted languages, interpreted languages make much slower programs than compiled languages.
The bottom line is, no matter what language you are writing in, at some point this has to be changed into 0's and 1's which can be understood by the computer. But the variables which you store in memory are not simply 0's and 1's. So you have to have a way in your programming languages of telling the computer whether or not the 0's and 1's should be treated as decimal numbers or characters or strings or anything else. You do this through the use of data types.
For example, say you have a variable which you call 'x' and you give it the decimal whole number value 65. In LPC you would do this through the statement:
-----x = 65;----- You can later do things like: -----
write(x+"\n"); /* \n is symbolically represents a carriage return */ y = x + 5;-----
The first line allows you to send 65 and a carriage return to someone's screen. The second line lets you set the value of y to 70. The problem for the computer is that it does not know what '65' means when you tell it x = 65;. What you think of 65, it might think of as:
00000000000000000000000001000001But, also, to the computer, the letter 'A' is represented as:
00000000000000000000000001000001So, whenever you instruct the computer write(x+"\n");, it must have some way of knowing that you want to see '65' and not 'A'.
The computer can tell the difference between '65' and 'A' through the use of data types. A data types simply says what type of data is being stored by the memory location pointed to by a given variable. Thus, each LPC variable has a variable type which guides conversions. In the example given above, you would have had the following line somewhere in the code *before* the lines shown above:
-----int x;-----
This one line tells the driver that whatever value x points to, it will be used as the data type "int", which is short for integer, or whole number. So you have a basic introduction into the reason why data types exist. They exist so the driver can make sense of the 0's and 1's that the computer is storing in memory.
All LPMud drivers have the following data types:
void, status, int, string, object, int *, string *, object *, mixed *
Many drivers, but not all have the following important data types which are important to discuss:
float, mapping, float *, mapping *<* Kingdoms have these datatypes. For an explanation on how mappings and *>
And there are a few drivers with the following rarely used data types which are not important to discuss:
function, enum, struct, char<* Kingdoms' driver don't have these types. - Ceril *>
This introductory textbook will deal with the data types void, status, int, float, string, object, mand mixed. You can find out about the more complex data types like mappings and arrays in the intermediate textbook. This chapter deals with the two simplest data types (from the point of view of the LPC coder), int and string.
An int is any whole number. Thus 1, 42, -17, 0, -10000023 are all type int. A string is one or more alphanumeric characters. Thus "a", "we are borg", "42", "This is a string" are all strings. Note that strings are always enclosed in "" to allow the driver to distinguish between the int 42 and the string "42" as well as to distinguish between variable names (like x) and strings by the same names (like "x").
When you use a variable in code, you must first let the driver know what type of data to which that variable points. This process is called *declaration*. You do this at the beginning of the function or at the beginning of the object code (outside of functions before all functions which use it). This is done by placing the name of the data type before the name of the variable like in the following example:
-----void add_two_and_two() { int x; int y; x = 2; y = x + x; }-----
Now, this is a complete function. The name of the function is add_two_and_two(). The function begins with the declaration of an int variable named x followed by the declaration of an in variable named y. So now, at this point, the driver now has two variables which point to NULL values, and it expects what ever values end up there to be of type int.
A note about the data types void and status:
Void is a trivial data type which points to nothing. It is not used
with respect to variables, but instead with respect to functions. You
will come to understand this better later. For now, you need only
understand that it points to no value.
The data type status is a boolean data type. That is, it can only have 1 or 0 as a value. This is often referred to as being true or false.
For variables, the driver needs to know how the 0's and 1's the computer stores in memory get converted into the forms in which you intend them to be used. The simplest LPC data types are void, status, int, and string. You do not user variables of type void, but the data type does come into play with respect to functions. In addition to being used for translation from one form to the next, data types are used in determining what rules the driver uses for such operations as +, -, etc. For example, in the expression 5+5, the driver knows to add the values of 5 and 5 together to make 10. With strings however, the rules for int addition make no sense. So instead, with "a"+"b", it appends "b" to the string "a" so that the final string is "ab". Errors can thus result if you mistakenly try to add "5"+5. Since int addition makes no sense with strings, the driver will convert the second 5 to "5" and use string addition. The final result would be "55". If you were looking for 10, you would therefore have ended up with erroneous code. Keep in mind, however, that in most instances, the driver will not do something so useful as coming up with "55". It comes up with "55" cause it has a rule for adding a string to an int, namely to treat the int as a string. In most cases, if you use a data type for which an operation or function is not defined (like if you tried to divide "this is" by "nonsense", "this is"/"nonsense"), the driver will barf and report an error to you.
By this point, you should be aware that LPC objects consist of functions which manipulate variables. The functions manipulate variables when they are executed, and they get executed through *calls* to those functions. The order in which the functions are placed in a file does not matter. Inside a function, the variables get manipulated. They are stored in computer memory and used by the computer as 0's and 1's which get translated to and from useable output and input through a device called data typing. String data types tell the driver that the data should appear to you and come from you in the form of alphanumeric characters. Variables of type int are represented to you as whole number values. Type status is represented to you as either 1 or 0.
And finally type void has no value to you or the machine, and is not really used with variable data types.
void do_nothing() { }-----
This function accepts no input, performs no instructions, and returns no value.
There are three parts to every properly written LPC function:
Like with variables, functions must be declared. This will allow the driver to know 1) what type of data the function is returning as output, and 2) how many input(s) and of what type those input(s) are. The more common word for input is parameters. A function declaration therefore consists of:
type name( parameter1, parameter2, ..., parameterN );
The declaration of a function called drink_water() which accepts a string as input and an int as output would thus look like this:
-----int drink_water(string str);-----
where str is the name of the input as it will be used inside the function.
The function definition is the code which describes what the function actually does with the input sent to it.
The call is any place in other functions which invokes the execution of the function in question. For two functions write_vals() and add(), you thus might have the following bit of code:
-----/* First, function declarations. They usually appear at the beginning of object code. */ void write_vals(); int add(int x, int y); /* Next, the definition of the function write_vals(). We assume that this function is going to be called from outside the object */ void write_vals() { int x; /* Now we assign x the value of the output of add() through a call */ x = add(2, 2); write(x + "\n"); } /* Finally, the definition of add() */ int add(int x, int y) { return (x + y); }-----
Remember, it does not matter which function definition appears first in the code. This is because functions are not executed consecutively. Instead, functions are executed as called. The only requirement is that the declaration of a function appear before its definition and before the definition of any function which makes a call to it.
Perhaps you have heard people refer to efuns. They are externally defined functions. Namely, they are defined by the mud driver. If you have played around at all with coding in LPC, you have probably found some expressions you were told to use like this_player(), write(), say(), this_object(), etc. look a lot like functions. That is because they are efuns. The value of efuns is that they are much faster than LPC functions, since they already exist in the binary form the computer understands.
In the function write_vals() above, two functions calls were made. The first was to the functions add(), which you declared and defined. The second call, however, was to a function called write(), and efun. The driver has already declared and defined this function for you. You needs only to make calls to it.
Efuns are created to handle common, every day function calls, to handle input/output to the internet sockets, and other matters difficult to be dealt with in LPC. They are written in C in the game driver and compiled along with the driver before the mud comes up, making them much faster in execution. But for your purposes, efun calls are just like calls made to your functions. Still, it is important to know two things of any efun: 1) what return type does it have, and 2) what parameters of what types does it take.
Information on efuns such as input parameters and return types is often found in a directory called /doc/efun on your mud. I cannot detail efuns here, because efuns vary from driver to driver. However, you can often access this information using the commands "man" or "help" depending on your mudlib. For instance, the command "man write" would give you information on the write efun. But if all else fails, "more /doc/efun/write" should work.
<* more /doc/build/efun/write *>By looking it up, you will find write is declared as follows:
-----void write(string);-----
This tells you an appropriate call to write expects no return value and passes a single parameter of type string.
Although ordering your functions within the file does not matter, ordering the code which defines a function is most important. Once a function has been called, function code is executed in the order it appears in the function definition. In write_vals() above, the instruction:
-----x = add(2, 2);-----
must come before the write() efun call if you want to see the appropriate value of x used in write().
With respect to values returned by function, this is done through the "return" instruction followed by a value of the same data type as the function. In add() above, the instruction is "return (x+y);", where the value of (x+y) is the value returned to write_vals() and assigned to x. On a more general level, "return" halts the execution of a function and returns code execution to the function which called that function. In addition, it returns to the calling function the value of any expression that follows. To stop the execution of a function of type void out of order, use "return"; without any value following. Once again, remember, the data type of the value of any expression returned using "return" MUST be the same as the data type of the function itself.
The files which define LPC objects are made of of functions. Functions, in turn, are made up of three parts:
Function declarations generally appear at the top of the file before any defintions, although the requirement is that the declaration must appear before the function definition and before the definition of any function which calls it.
Function definitions may appear in the file in any order so long as they come after their declaration. In addition, you may not define one function inside another function.
Function calls appear inside the definition of other functions where you want the code to begin execution of your function. They may also appear within the definition of the function itself, but this is not recommended for new coders, as it can easily lead to infinite loops.
The function definition consists of the following in this order:
The trivial function would thus be:
-----void do_nothing() {}-----
since this function does not accept any input, perform any instructions, or return any output.
Any function which is not of type void MUST return a value of a data type matching the function's data type.
Each driver has a set of functions already defined for you called efuns These you need neither need to declare nor define since it has already been done for you. Furthermore, execution of these functions is faster than the execution of your functions since efuns are in the driver. In addition, each mudlib has special functions like efuns in that they are already defined and declared for you, but different in that they are defined in the mudlib and in LPC. They are called simul_efuns, or simulated efuns. You can find out all about each of these as they are listed in the /doc/efun directory on most muds. In addition many muds have a command called "man" or a "help" command which allows you simply to call up the info files on them.
Note on style:
Some drivers may not require you to declare your functions, and some
may not require you to specify the return type of the function in its
definition. Regardless of this fact, you should never omit this information
for the following reasons:
You should now understand the basic workings of functions. You should be able to declare and call one. In addition, you should be able to recognize function definitions, although, if this is your first experience with LPC, it is unlikely that you will as yet be able to define your own functions. The functions form the basic building blocks of LPC objects. Code in them is executed when another function makes a call to them. In making a call, input is passed from the calling function into the execution of the called one. The called function then executes and returns a value of a certain data type to the calling function. Functions which return no value are of type void.
After examining your workroom code, it might look something like this (depending on the mudlib):
-----inherit "/std/room"; void create() { ::create(); set_property("light", 2); set_property("indoors", 1); set("short", "Descartes' Workroom"); set("long", "This is where Descartes works.\nIt is a cube.\n"); set_exits( ({ "/d/standard/square" }), ({ "square" }) ); }-----
inherit "/std/room/room"; void create_object() { ::create_object(); set_light(1); set_property("indoors"); set_short("Descartes' Workroom"); set_long("This is where Descartes works. It is a cube.\n"); add_exit("/room/adv_guild","east"); }<* - Ceril *>
If you understand the entire textbook to this point, you should recognize of the code the following:
This chapter will seek to answer the questions that should be in your head at this point:
Inheritance is one of the properties which define true object oriented programming (OOP). It allows you to create generic code which can be used in many different ways by many different programs. What a mudlib does is create these generalized files (objects) which you use to make very specific objects.
If you had to write the code necessary for you to define the workroom above, you would have to write about 1000 lines of code to get all the functionality of the room above. Clearly that is a waste of disk space. In addition, such code does not interact well with players and other rooms since every creator is making up his or her own functions to perform the functionality of a room. Thus, what you might use to write out the room's long description, query_long(), another wizard might be calling long(). This is the primary reason mudlibs are not compatible, since they use different protocols for object interaction.
OOP overcomes these problems. In the above workroom, you inherit the functions already defined in a file called "/std/room.c". It has all the functions which are commonly needed by all rooms defined in it. When you get to make a specific room, you are taking the general functionality of that room file and making a unique room by adding your own function, create().
As you might have guessed by now, the line:
-----inherit "/std/room"; <* "/std/room/room" - Ceril *>-----
has you inherit the functionality of the room "/std/room.c". By inheriting the functionality, it means that you can use the functions which have been declared and defined in the file "/std/room.c" In the Nightmare Mudlib, "/std/room.c" has, among other functions, set_property(), set(), and set_exits() declared and defined. In your function create(), you are making calls to those functions in order to set values you want your room to start with. These values make your room different from others, yet able to interact well with other objects in memory.
In actual practice, each mudlib is different, and thus requires you to use a different set of standard functions, often to do the same thing. It is therefore beyond the scope of this textbook even to describe what functions exist and what they do. If your mudlib is well documented, however, then (probably in /doc/build) you will have tutorials on how to use the inheritable files to create such objects. These tutorials should tell you what functions exist, what input they take, the data type of their output, and what they do.
This is far from a complete explanation of the complex subject of inheritance. The idea here is for you to be able to understand how to use inheritance in creating your objects. A full discussion will follow in a later textbook. Right now you should know the following:
inherit "filename";-----
Note:
You may see the syntax ::create() or ::init() or ::reset() in places.
You do not need fully to understand at this point the full nuances of this,
but you should have a clue as to what it is. The "::" operator is a way
to call a function specifically in an inherited object (called the scope
resolution operator). For instance, most muds' room.c has a function
called create(). When you inherit room.c and configure it, you are doing
what is called overriding the create() function in room.c. This means
that whenever ANYTHING calls create(), it will call *your* version and not
the one in room.c. However, there may be important stuff in the room.c
version of create(). The :: operator allows you to call the create() in
room.c instead of your create().
An example:
-----/* #1 */ inherit "/std/room"; void create() { create(); }-----
-----
/* #2 */ inherit "/std/room"; void create() { ::create(); }-----
Example 1 is a horror. When loaded, the driver calls create(), and then create() calls create(), which calls create(), which calls create()... In other words, all create() does is keep calling itself until the driver detects a too deep recursion and exits.
Example 2 is basically just a waste of RAM, as it is no different from room.c functionally. With it, the driver calls its create(), which in turn calls ::create(), the create() in room.c. Otherwise it is functionally exactly the same as room.c.
By now you should be able to code some simple objects using your muds standard object library. Inheritance allows you to use functions defined in those objects without having to go and define yourself. In addition, you should know how to declare your own functions. This chapter will teach you about the basic elements of LPC which will allow you to define your own functions using the manipulation of variables.
Basically, what makes objects on the mud different are two things:
Now, all player objects have the same functions. They are therefore differentiated by the values they hold. For instance, the player named "Forlock" is different from "Descartes" *at least* in that they have different values for the variable true_name, those being "descartes" and "forlock".
Therefore, changes in the game involve changes in the values of the objects in the game. Functions are used to name specific process for manipulating values. For instance, the create() function is the function whose process is specifically to initialize the values of an object. Within a function, it is specifically things called instructions which are responsible for the direct manipulation of variables.
Like variables in most programming language, LPC variables may be declared as variables "local" to a specific function, or "globally" available to all functions. Local variables are declared inside the function which will use them. No other function knows aboutwill use them. No other function knows about their existence, since the values are only stored in memory while that function is being executed. A global variable is available to any function which comes after its declaration in the object code. Since global variables take up RAM for the entire existence of the object, you should use them only when you need a value stored for the entire existence of the object. Have a look at the following 2 bits of code:
-----int x; int query_x() { return x; } void set_x(int y) { x = y; }-----
-----
void set_x(int y) { int x; x = y; write("x is set to x"+x+" and will now be forgotten.\n"); }-----
In the first example, x is declared outside of any functions, and therefore
will be available to any function declared after it. In that example,
x is a global variable.
In the second example, x is declared inside the function set_x(). It
only exists while the function set_x() is being executed. Afterwards,
it ceases to exist. In that example, x is a local variable.
Instructions to the driver are used to manipulate the values of variables. An example of an instruction would be:
-----x = 5;-----
The above instruction is self-explanatory. It assigns to the variable x the value 5. However, there are some important concepts in involved in that instruction which are involved in instructions in general. The first involves the concept of an expression. An expression is any series of symbols which have a value. In the above instruction, the variable x is assigned the value of the expression 5. Constant values are the simplest forms in which expressions can be put. A constant is a value that never changes like the int 5 or the string "hello". The last concept is the concept of an operator. In the above example, the assignment operator = is used.
There are however many more operators in LPC, and expressions can get quite complex. If we go up one level of complexity, we get:
-----y = 5; x = y +2;-----
The first instruction uses the assignment operator to assign the value of the constant expression 5 to the variable y. The second one uses the assignment operator to assign to x the value of the expression ( y+2 ) which uses the addition operator to come up with a value which is the sum of the value of y and the value of the constant expression 2. Sound like a lot of hot air?
In another manner of speaking, operators can be used to form complex expressions. In the above example, there are two expressions in the one instruction x = y + 2;:
As stated before, all expressions have a value. The expression
y+2 has the value of the sum of y and 2 (here, 7);
The expression x = y + 2 *also* has the value of 7.
So operators have to important tasks:
Now, not all operators do what 1 does. The = operators does act upon the value of 7 on its right by assigning that value to x. The operator + however does nothing. They both, however, have their own values.
As you may have noticed above, the expression x = 5 *itself* has a value of 5. In fact, since LPC operators themselves have value as expressions, they cal allow you to write some really convoluted looking nonsense like:
i = ( (x=sizeof(tmp=users())) ? --x : sizeof(tmp=children("/std/monster"))-1)which says basically:
assing to tmp the array returned by the efun users(), then assign to x the value equal to the number of elements to that array. If the value of the expression assigning the value to x is true (not 0), then assign x by 1 and assign the value of x-1 to i. If x is false though, then set tmp to the array returned by the efun children(), and then assign to i the value of the number of members in the array tmp -1.
Would you ever use the above statement? I doubt it. However you might see or use expressions similar to it, since the ability to consolidate so much information into one single line helps to speed up the execution of your code. A more often used version of this property of LPC operators would be something like:
-----x = sizeof(tmp = users()); while(i--) write((string)tmp[i]->query_name()+"\n");----- instead of writing something like: -----
tmp = users(); x = sizeof(tmp); for(i=0; i-----query_name()+"\n");
Things like for(), while(), arrays and such will be explained later. But the first bit of code is more concise and it executed faster.
NOTE: A detailed description of all basic LPC operators follows the chapter summary.
You now know how to declare variables and understand the difference between declaring and using them globally or locally. Once you become familiar with your driver's efuns, you can display those values in many different ways. In addition, through the LPC operators, you know how to change and evaluate the values contained in variables. This is useful of course in that it allows you to do something like count how many apples have been picked from a tree, so that once all apples have been picked, no players can pick more. Unfortunately, you do not know how to have code executed in anything other than a linera fashion. In other words, hold off on that apple until the next chapter, cause you do not know how to check if the apples picked is equal to the number of apples in the tree. You also do not know about the special function init() where you give new commands to players. But you are almost ready to code a nice, fairly complex area.
This section contains a detailed listing of the simpler LPC operators, including what they do to the values they use (if anything) and the value that they have.
The operators described here are:
= + - * / % += -= *= /= %= -- ++ == != > < >= <= ! && || -> ? :
Those operators are all described in a rather dry manner below, but it is best to at least look at each one, since some may not behave *exactly* as you think. But it should make a rather good reference guide.
A pair of more complicated ones that are here just for the sake of being here. Do not worry if they utterly confuse you.
A note on equality: A very nasty error people make that is VERY difficult to debug is the error of placing = where you mean ==. Since operators return values, they both make sense when being evaluated. In other words, no error occurs. But they have very different values. For example:
if(x == 5) if(x = 5)
The value of x == 5 is true if the value of x is 5, false othewise.
The value of x = 5 is 5 (and therefore always true).
The if statement is looking for the expression in () to be either true or
false, so if you had = and meant ==, you would end up with an expression that is
always true. And you would pull your hair out trying to figure out
why things were not happening like they should :)
Variables may be manipulated by assigning or changing values with the expressions =, +=, -=, ++, --. Those expressions may be combined with the expressions -, +, *, /, %. However, so far, you have only been shown how to use a function to do these in a linear way. For example:
int hello(int x) { x--; write("Hello, x is "+x+".\n"); return x; }
is a function you should know how to write and understand. But what if you wanted to write the value of x only if x = 1? Or what if you wanted it to keep writing x over and over until x = 1 before returning? LPC uses flow control in exactly the same way as C and C++.
Before we discuss these, first something on what is meant by expression and instruction. An expression is anything with a value like a variable, a comparison (like x>5, where if x is 6 or more, the value is 1, else the value is 0), or an assignment(like x += 2). An instruction can be any single line of lpc code like a function call, a value assignment or modification, etc.
You should know also the operators &&, ||, ==, !=, and !. These are the logical operators. They return a nonzero value when true, and 0 when false. Make note of the values of the following expressions:
In expressions using &&, if the value of the first item being compared is 0, the second is never tested even. When using ||, if the first is true (1), then the second is not tested.
The first expression to look at that alters flow control is if(). Take a look at the following example:
1 void reset() { 2 int x; 3 4 ::reset(); 5 x = random(100); 6 if(x > 50) set_search_func("floorboards", "search_floor"); 7 }
The line numbers are for reference only. In line 2, of course we declare a variable of type int called x. Line 3 is aesthetic whitespace to clearly show where the declarations end and the function code begins. The variable x is only available to the function reset().
Line 4 makes a call to the room.c version of reset().
Line 5 uses the driver efun random() to return a random number between 0 and the parameter minus 1. So here we are looking for a number between 0 and 99.
In line 6, we test the value of the expression (x>50) to see if it is true or false. If it is true, then it makes a call to the room.c function set_search_func(). If it is false, the call to set_search_func() is never executed.
In line 7, the function returns driver control to the calling function (the driver itself in this case) without returning any value.
If you had wanted to execute multiple instructions instead of just the one, you would have done it in the following manner:
if(x>50) { set_search_func("floorboards", "search_floor"); if(!present("beggar", this_object())) make_beggar(); }
Notice the {} encapsulate the instructions to be executed if the test expression is true. In the example, again we call the room.c function which sets a function (search_floor()) that you will later define yourself to be called when the player types "search floorboards" (NOTE: This is highly mudlib dependent. Nightmare mudlibs have this function call. Others may have something similar, while others may not have this feature under any name). Next, there is another if() expression that tests the truth of the expression (!present("beggar",this_object())). The ! in the test expression changes the truth of the expression which follows it. In this case, it changes the truth of the efun present(), which will return the object that is a beggar if it is in the room (this_object()), or it will return 0 if there is no beggar in the room. So if there is a beggar still living in the room, (present("beggar", this_object())) will have a value equal to the beggar object (data type object), otherwise it will be 0. The ! will change a 0 to a 1, or any nonzero value (like the beggar object) to a 0. Therefore, the expression (!present("beggar", this_object())) is true if there is no beggar in the room, and false if there is. So, if there is no beggar in the room, then it calls the function you define in your room code that makes a new beggar and puts it in the room. (If there is a beggar in the room, we do not want to add yet another one :))
Of course, if()'s often comes with ands or buts :). In LPC, the formal reading of the if() statement is:
if(expression) { set of intructions } else if(expression) { set of instructions } else { set of instructions }
This means:
If expression is true, then do these instructions.
Otherise, if this second expression is true, do this second set.
And if none of those were true, then do this last set.
if(x>5) write("Foo,\n");with an else if():
if(x > 5) write("X is greater than 5.\n"); else if(x >2) write("X is less than 6, but greater than 2.\n");with an else:
if(x>5) write("X is greater than 5.\n"); else write("X is less than 6.\n");or the whole lot of them as listed above. You can have any number of else if()'s in the expression, but you must have one and only one if() and at most one else. Of course, as with the beggar example, you may nest if() statements inside if() instructions. (For example,
if(x>5) { if(x==7) write("Lucky number!\n"); else write("Roll again.\n"); } else write("You lose.\n");
while(expression) { set of instructions } do { set of instructions } while(expression);
These allow you to create a set of instructions which continue to execute so long as some expression is true. Suppose you wanted to set a variable equal to a player's level and keep subtracting random amounts of either money or hp from a player until that variable equals 0 (so that player's of higher levels would lose more). You might do it this way:
1 int x; 2 3 x = (int)this_player()->query_level(); /* this has yet to be explained */ 4 while(x > 0) { 5 if(random(2)) this_player()->add_money("silver", -random(50)); 6 else this_player()->add_hp(-(random(10)); 7 x--; 8 }<* On Kingdoms such a piece of code would look like: *>
int x; x = this_player()->query_level(); /* this has yet to be explained */ while( x > 0 ) { if(random(2)) { this_player()->add_money(-random(50)); } else { this_player()->reduce_hit_point(-random(10)); } x--; }<* - Ceril *>
The expression this_player()->query_level() calIn line 4, we start a loop that executes so long as x is greater than 0.
Another way we could have done this line would be: while(x) { The problem with that would be if we later made a change to the funtion y anywhere between 0 and 49 coins. In line 6, if instead it returns 0, we call the add_hp() function in the player which reduces the player's hit points anywhere between 0 and 9 hp. In line 7, we reduce x by 1. At line 8, the execution comes to the end of the while() instructions and goes back up to line 4 to see if x is still greater than 0. This loop will keep executing until x is finally less than 1.You might, however, want to test an expression *after* you execute some instructions. For instance, in the above, if you wanted to execute the instructions at least once for everyone, even if their level is below the test level:
int x; x = (int)this_player()->query_level(); do { if(random(2)) this_player()->add_money("silver", -random(50)); else this_player()->add_hp(-random(10)); x--; } while(x > 0);<* Which on Kingdoms would be: *>
int x; x = this_player()->query_level(); do { if(random(2)) { this_player()->add_money(-random(50)); } else { this_player()->reduce_hit_point(-random(10)); } x--; } while(x > 0);-----
This is a rather bizarre example, being as few muds have level 0 players. And even still, you could have done it using the original loop with a different test. Nevertheless, it is intended to show how a do{} while() works. As you see, instead of initiating the test at the beginning of the loop (which would immediately exclude some values of x), it tests after the loop has been executed. This assures that the instructions of the loop get executed at least one time, no matter what x is.
Prototype:
for(initialize values ; test expression ; instruction) { instructions }
This allows you to set starting values of variables which will be used in the loop. This part is optional.
Same as the expression in if() and while(). The loop is executed as long as this expression (or expressions) is true. You must have a test expression.
An expression (or expressions) which is to be executed at the end of each loop. This is optional.
Note:
for(;expression;) {}
IS EXACTLY THE SAME AS
while(expression) {}
Example:
1 int x; 2 3 for(x= (int)this_player()->query_level(); x>0; x--) { 4 if(random(2)) this_player()->add_money("silver", -random(50)); 5 else this_player()->add_hp(-random(10)); 6 }
This for() loop behaves EXACTLY like the while() example. Additionally, if you wanted to initialize 2 variables:
for(x = 0, y = random(20); x < y; x++) { write(x + "\n"); }
Here, we initialize 2 variables, x and y, and we separate them by a comma. You can do the same with any of the 3 parts of the for() expression.
Prototype:
switch(expression) { case constant: instructions case constant: instructions ... case constant: instructions default: instructions }
This is functionally much like if() expressions, and much nicer to the CPU, however most rarely used because it looks so damn complicated. But it is not.
First off, the expression is not a test. The cases are tests. A English sounding way to read:
1 int x; 2 3 x = random(5); 4 switch(x) { 5 case 1: write("X is 1.\n"); 6 case 2: x++; 7 default: x--; 8 } 9 write(x+"\n");
is:
set variable x to a random number between 0 and 4.
In case 1 of variable x write its value add 1 to it and subtract 1.
In case 2 of variable x, add 1 to its value and then subtract 1.
In other cases subtract 1.
Write the value of x.
switch(x) basically tells the driver that the variable x is the value we are trying to match to a case. Once the driver finds a case which matches, that case *and all following cases* will be acted upon. You may break out of the switch statement as well as any other flow control statement with a break instruction in order only to execute a single case. But that will be explained later. The default statement is one that will be executed for any value of x so long as the switch() flow has not been broken. You may use any data type in a switch statement:
string name; name = (string)this_player()->query_name(); switch(name) { case "descartes": write("You borg.\n"); case "flamme": case "forlock": case "shadowwolf": write("You are a Nightmare head arch.\n"); default: write("You exist.\n"); }
For me, I would see:
You borg. You are a Nightmare head arch. You exist.
Flamme, Forlock, or Shadowwolf would see:
You are a Nightmare head arch. You exist.
Everyone else would see:
You exist.
The following instructions:
return continue break
alter the natural flow of things as described above. First of all,
return
no matter where it occurs in a function, will cease the execution of that function and return control to the function which called the one the return statement is in. If the function is NOT of type void, then a value must follow the return statement, and that value must be of a type matching the function. An absolute value function would look like this:
int absolute_value(int x) { if(x>-1) return x; else return -x; }
In the second line, the function ceases execution and returns to the calling function because the desired value has been found if x is a positive number.
continue
is most often used in for() and while statements. It
serves to stop the execution of the current loop and send the execution back
to the beginning of the loop. For instance, say you wanted to avoid
division by 0:
x = 4; while( x > -5) { x-- if(!x) continue; write((100/x) + "\n"); } write("Done.\n")
You would see the following output:
33 50 100 -100 -50 -33 -25 Done.
To avoid an error, it checks in each loop to make sure x is not 0. If x is zero, then it starts back with the test expression without finishing its current loop.
In a for() expression
for(x=3; x>-5; x--) { if(!x) continue; write((100/x)+"\n"); } write("Done.\n");
It works much the same way. Note this gives exactly the same output as before. At x=1, it tests to see if x is zero, it is not, so it writes 100/x, then goes back to the top, subtracts one from x, checks to see if it is zero again, and it is zero, so it goes back to the top and subtracts 1 again.
break
This one ceases the function of a flow control statement. No matter
where you are in the statement, the control of the program will go
to the end of the loop. So, if in the above examples, we had
used break instead of continue, the output would have looked like this:
33 50 100 Done.
continue is most often used with the for() and while() statements. break however is mostly used with switch()
switch(name) { case "descartes": write("You are borg.\n"); break; case "flamme": write("You are flamme.\n"); break; case "forlock": write("You are forlock.\n"); break; case "shadowwolf": write("You are shadowwolf.\n"); break; default: write("You will be assimilated.\n"); }
This functions just like:
if(name == "descartes") write("You are borg.\n"); else if(name == "flamme") write("You are flamme.\n"); else if(name == "forlock") write("You are forlock.\n"); else if(name == "shadowwolf") write("You are shadowwolf.\n"); else write("You will be assimilated.\n");
except the switch statement is much better on the CPU. If any of these are placed in nested statements, then they alter the flow of the most immediate statement.
This chapter covered one hell of a lot, but it was stuff that needed to be seen all at once. You should now completely understand if() for() while() do{} while() and switch(), as well as how to alter their flow using return, continue, and break. Effeciency says if it can be done in a natural way using switch() instead of a lot of if() else if()'s, then by all means do it. You were also introduced to the idea of calling functions in other objects. That however, is a topic to be detailed later. You now should be completely at ease writingYou now should be completely at ease writing simple rooms (if you have read your mudlib's room building document), simple monsters, and other sorts of simple objects.
You should now be able to do anything so long as you stick to calling functions within your own object. You should also know, that at the bare minimum you can get the create() (or reset()) function in your object called to start just by loading it into memory, and that your reset() function will be called every now and then so that you may write the code necessary to refresh your room. Note that neither of these functions MUST be in your object. The driver checks to see if the function exists in your object first. If it does not, then it does not bother. You are also acquainted with the data types void, int, and string.
In this chapter you will be acquainted with a more complex data type, object. An object variable points to a real object loaded into the driver's memory. You declare it in the same manner as other data types:
object ob;
It differs in that you cannot use +, -, +=, -=, *, or / (what would it mean to divide a monster by another monster?). And since efuns like say() and write() only want strings or ints, you cannot write() or say() them (again, what would it mean to say a monster?). But you can use them with some other of the most important efuns on any LPMud.
This is an efun which returns an object in which the function being executed exists. In other words, in a file, this_object() refers to the object your file is in whether the file gets cloned itself or inherted by another file. It is often useful when you are writing a file which is getting inherited by another file. Say you are writing your own living.c which gets inherited by user.c and monster.c, but never used alone. You want to log the function set_level() if it is a player's level being set (but you do not care if it is a monster.
You might do this:
void set_level(int x) { if(this_object()->is_player()) log_file("levels", "foo\n"); level = x; }
Since is_player() is not defined in living.c or anything it inherits, just saying if(is_player()) will result in an error since the driver does not find that function in your file or anything it inherits. this_object() allows you to access functions which may or may not be present in any final products because your file is inherited by others without resulting in an error.
This of course introduces us to the most important characteristic of the object data type. It allows us to access functions in other objects. In previous examples you have been able to find out about a player's level, reduce the money they have, and how much hp they have.
Calls to functions in other objects may be done in two ways:
object->function(parameters) call_other(object, "function", parameters);
example:
this_player()->add_money("silver", -5); call_other(this_player(), "add_money", "silver", -5);
<* On Kingdoms: *> <* this_player()->add_money(-5); *> <* call_other(this_player(), "add_money",-5); - Ceril *>
In some (very loose sense), the game is just a chain reaction of function calls initiated by player commands. When a player initiates a chain of function calls, that player is the object which is returned by the efun this_player(). So, since this_player() can change depending on who initiated the sequence of events, you want to be very careful as to where you place calls to functions in this_player(). The most common place you do this is through the last important lfun (we have mentioned create() and reset()) init().
Any time a living thing encounters an object (enters a new room, or enters the same room as a certain other object), init() is called in all of the objects the living being newly encounters. It is at this point that you can add commands the player can issue in order to act. Here is a sample init() function in a flower.
void init() { ::init(); add_action("smell_flower", "smell"); }
Ito smell_flower(). So you should have smell_flower() look like this:
1 int smell_flower(string str); /* action functions are type int */ 2 3 int smell_flower(string str) { 4 if(str != "flower") return 0; /* it is not the flower being smelled */ 5 write("You sniff the flower.\n"); 6 say((string)this_player()->query_cap_name()+" smells the flower.\n"); 7 this_player()->add_hp(random(5)); 8 return 1; 9 }
<* On Kingdoms it would look like this: *>
1 int smell_flower(string str); 2 3 int smell_flower(string str) { 4 if(str != "flower") { return 0; } 5 write("You sniff the flower.\n"); 6 say(this_player()->query_name() + " smells the flower.\n"); 7 this_player()->reduce_hit_point(-random(5)); 8 return 1; 9}<* - Ceril *>
In line 1, we have our function declared.
In line 3, smell_flower() begins. str becomes whatever comes after the
players command (not including the first white space).
In line 4, it checks to see if the player had typed "smell flower". If
the player had typed "smell cheese", then str would be "cheese". If
it is not in fact "flower" which is being smelled, then 0 is returned,
letting the driver know that this was not the function which should
have been called. If in fact the player had a piece of cheese as well
which had a smell command to it, the driver would then call the function
for smelling in that object. The driver will keep calling all functions
tied to smell commands until one of them returns 1. If they all return
0, then the player sees "What?"
In line 5, the efun write() is called. write() prints the string which
is passed to it to this_player(). So whoever typed the command here
sees "You sees "You sniff the flower."
In line 6, the efun say() is called. say() prints the string which is
doing the sniffing, we have to call the query_cap_name() function
in this_player(). That way if the player is invis, it will say
"Someone" (or something like that), and it will also be properly
capitalized.
In line 7, we call the add_hp() function in the this_player() object,
since we want to do a little healing for the sniff (Note: do not
code this object on your mud, whoever balances your mud will shoot you).
In line 8, we return control of the game to the driver, returning 1 to
let it know that this was in fact the right function to call.
And now, using the data type object, you can add monsters to your rooms:
void create() { ::create(); set_property("light", 3); set("short", "Krasna Square"); set("long", "Welcome to the Central Square of the town of Praxis.\n"); set_exits( ({ "d/standard/hall" }), ({ "east" }) ); } void reset() { object ob; ::reset(); if(present("guard")) return; /* Do not want to add a guard if */ ob = new("/std/monster"); /* one is already here */ ob->set_name("guard"); ob->set("id", ({ "guard", "town guard" }) ); ob->set("short", "Town guard"); ob->set("long", "He guards Praxis from nothingness.\n"); ob->set_gender("male"); ob->set_race("human"); ob->set_level(10); ob->set_alignment(200); ob->set_humanoid(); ob->set_hp(150); ob->set_wielding_limbs( ({ "right hand", "left hand" }) ); ob->move(this_object()); }
Now, this will be wildly different on most muds. Some, as noted before, in that object so you have a uniquely configured monster object. The last act in native muds is to call move() in the monster object to move it to this room (this_object()). In compat muds, you call the efun move_object() which takes two parameters, the object to be moved, and the object into which it is being moved.
<* On Kingdoms we prefer you to code your monsters as separate files.- Ceril*>
At this point, you now have enough knowledge to code some really nice stuff. Of course, as I have been stressing all along, you really need to read the documents on building for your mud, as they detail which functions exist in which types of objects for you to call. No matter what your knowledge of the mudlib is, you have enough know-how to give a player extra things to do like sniffing flowers or glue or whatever. At this point you should get busy coding stuff. But the moment things eveneven look to become tedious, that means it is time for you to move to the next level and do more. Right now code yourself a small area. Make extensive use of the special functions coded in your mud's room.c (search the docs for obscure ones no one else seems to use). Add lots o' neat actions. Create weapons which have magic powers which gradually fade away. All of this you should be able to do now. Once this becomes routine for you, it will be time to move on to intermediate stuff. Note that few people actually get to the intermediate stuff. If you have played at all, you notice there are few areas on the mud which do what I just told you you should be able to do. It is not because it is hard, but because there is a lot of arrogance out there on the part of people who have gotten beyond this point, and very little communicating of that knowledge. The trick is to push yourself and think of something you want to do that is impossible. If you ask someone in the know how to do X, and they say that is impossible, find out yourself how to code it by experimenting.
George Reese
Descartes of Borg
12 july 1993
borg@hebron.connected.com
Descartes@Nightmare (intermud)
Descartes@Igor (not intermud)
Intermediate LPC Descartes of Borg November 1993 Contents 1: Introduction 2: The LPMud Driver 3: Complex Data Types 4: The LPC Pre-Compiler 5: Advanced String Handling 6: Intermediate Inheritance 7: Debugging Copyright (c) George Reese 1993 Intermediate LPC first edition Copyright (c) 1993 George Reese All rights to this text are retained by the author. Permission is granted to distrubute and display the contents of this document in full so long as the following conditions are met: 1) No payment may be received for the redistribution or display of this text, except to cover the costs for distribution media and and shipping and/or transmission charges. 2) The textbook must be distributed or displayed in its entirety in its original form. Changes may only be made to private, individual copies, except as outlined below. Acceptable changes are defined as the following: 1) Format changes, such as changing from WordPerfect to Word 2) Medium changes, such as from electronic copy to paper 3) Content changes are only acceptable under the following circumstances: a) In electronic a) In electronic media: none of the original text may be ommitted. You may add comments as you feel necessary, so long as comments are enclosed in <* *> and are accompanied by the game name or real name of the author of the comments b) In hard copy: none of the original text may be omitted, but it may be struck out so long as the content of the original text is visible. Comments may be made in any form so long as they are made in handwriting and they are signed by the author. Comments which are typed or printed must be made in accordance with the format for electronic media. Practically speaking, this is what I mean: First, I wrote this mostly for mud admins to put onto their muds for learning coders to read as they are learning to build realms. I did not do this for someone else to make a buck. So if you charge money for redistributing it or allowing someone else to see it, you are in violation of this copyright. Unless you are simply charging for what it cost you to print up a copy or what the diskettes and postage cost to mail it. Second, I wrote this textbook, and I should receive credit/blame for what I say, and others should receive credit/blame for what they say. For example, if I said something completely wrong, and you simply corrected it, I would be getting credit for something I did not do. Yet, if you comment according to the outline above, you will be properly credited for your comments. More important to me, however, is the practical effect of having hundreds of copies of this textbook everywhere. If you change something I had right without noting it as a comment, I will be blamed for spreading misinformation. This problem is only compunded if the text is redistributed. So I prefer my words to remain my own. But, when I make mistakes, or if something I say does not fit your driver/mudlib, please comment it so people will know. In addition, having the comments side-by-side allows people to see othersee other ideas, like how another driver might handle something. I want to please note again, you may display this on your mud (like in /doc). You do not need to mail me for permission. I would not mind email, since it is nice to know people are using it, but that is not required. Also, if you really feel I have done such a wonderful job that you should pay money to use this, then give $5 to your local ASPCA (or international equivalent). See the file titled Contents for a full list of textbook chapters. Intermediate LPC Descartes of Borg November 1993 Chapter 1: Introduction 1.1 LPC Basics Anyone reading this textbook should either have read the textbook LPC Basics or be familiar enough with mud realm coding such that not only are they capable of building rooms and other such objects involved in area coding, but they also have a good idea of what is going on when the code they write is executing. If you do not feel you are at this point, then go back and read LPC Basics before continuing. If you do so, you will find that what you read here will be much more meaningful to you. 1.2 Goals of This Textbook The introductory textbook was meant to take people new to LPC from knowing nothing to being able to code a nice realm on any LPMud. There is naturally much more to LPC and to LPMud building, however, than building rooms, armours, monsters, and weapons. As you get into more complicated concepts like guilds, or desire to do more involved things with your realm, you will find the concepts detailed in LPC Basics to be lacking in support for these projects. Intermediate LPC is designed to take you beyond the simple realm building process into a full knowledge of LPC for functioning as a realm builder on an LPMud. The task of mudlib building itself is left to a later text. After reading this textbook and working through it by experimenting with actual code, the reader should be able to code game objects to fit any design or idea they have in mind, so long as I have been successful. 1.3 An Overview What more is there? Well many of you are quite aware that LPC supports mappings and arrays and have been asking me why those were not detailed in LPC Basics. I felt that those concepts were beyond the scope of what I was trying to do with that textbook and were more fitting to this textbook. But new tools are all fine and dandy, what matters, however, is what you can do with those tools. The goal of LPC Basics was to get you to building quality LPMud realms. Mappings and arrays are not necessary to do that. The goal of this book is to allow you to code any idea you might want to code in your area. That ability requires the knowledge of mappings and arrays. Any idea you want to code in an LPMud is possible. LPC is a language which is amazingly well suited to this task. All that prevents you from coding your ideas is your knowledge of LPC or an inadequate mudlib or your mud s theme or administrative policies. This textbook cannot make the mudlib you are working with any better, and it cannot change the mud theme or the mud s administrative policies. Never once think that LPC is incapable of doing what you want to do. If your idea is prevented by administrative policies or themes, then it is simply not an idea for your current mud. If the mudlib is inadequate, talk to the people in charge of your mudlib about what can be done at the mudlib level to facilitate it. You would be surprised by what is actually in the mudlib you did not know about. More important, after reading this textbook, you should be able to read all of the mudlib code in your mud s mudlib and understand what is going on at each line in the mudlib code. You may not as yet be able to reproduce that code on your own, but at least you can understand what is going on at the mudlib level. This textbook starts out with a discussion about what the LPMud driver is doing. One nice thing about this textbook, in general it is completely driver and mudlib independent (excepting for the Dworkin Game Driver). The chapter on the game driver does not get into actual implementation, but instead deals with what all game drivers basically do in order to run the mud. Next I discuss those magic topics everyone wants to know more about, arrays and mappings. Mappings may be simultaneously the easiest and most difficult data type to understand. Since they are sort of complex arrays in a loose sense, you really need to understand arrays before discussing them. All the same, once you understand them, they are much easier than arrays to use in real situations. At any rate, spend most of your time working with that chapter, because it is probably the most difficult, yet most useful chapter in the book. After that follows a brief chapter on the LPC pre-compiler, a tool you can use for sorting out how your code will look before it gets sent to the compiler. Despite my horrid intro to it here, this chapter is perhaps the easiest chapter in the textbook. I put it after the mappings and arrays chapter for exactly that reason. Strings are re-introduced next, going into more detail with how you can do such things as advanced command handling by breaking up strings. Once you understand arrays fairly well, this chapter should be really simple. The next chapter is the second most important in the book. It may be the most important if you ever intend to go beyond the intermediate stage and dive into mudlib coding. That chapter involves the complex ideas behind LPC inheritance. Since the goal of this textbook is not to teach mudlib programming, the chapter is not a detailed discussion on object oriented programming. Understanding this chapter, however, will give you some good insights into what is involved with object oriented programming, as well as allow you to build more complex objects by overriding functions and defining your own base classes. Finally, the textbook ends with a simple discussion of code debugging. This is not an essential chapter, but instead it is meant as more of an auxiliary supplement to what the knowledge you have accumulated so far. 1.4 Not Appearing in This Textbook Perhaps what might appear to some as the most glaring omission of this textbook is largely a political omission, shadows. Never have I ever encountered an example of where a shadow was either the best or most effecient manner of doing anything. It does not follow from that, however, that there are no uses for shadows. My reasoning for omitting shadows from this textbook is that the learner is best served by learning the concepts in this textbook first and having spent time with them before dealing with the subject of shadows. In that way, I feel the person learning LPC will be better capable of judging the merits of using a shadow down the road. I will discuss shadows in a future textbook. If you are someone who uses shadows some or a lot, please do not take the above paragraph as a personal attack. There may be some perfectly valid uses for shadows somewhere which I have yet to encounter. Nevertheless, they are not the ideal way to accomplish any given task, and therefore they are not considered for the purposes of this textbook an intermediate coding tool. I have also omitted discussions of security and object oriented programming. Both are quite obviously mudlib issues. Many people, however, might take exception with my leaving out a discussion of object oriented programming. I chose to leave that for a later text, since most area builders code for the creativity, not for the computer science theory. In both the intermediate and beginner textbooks, I have chosen only to discuss theory where it is directly applicable to practical LPC programming. For people who are starting out green in LPC and want to code the next great mudlib, perhaps theory would be more useful. But for the purposes of this book, a discussion of object oriented programming is simply a snoozer. I do plan to get heavy into theory with the next textbook. 1.5 Summary LPC is not difficult to learn. It is a language which, although pathetic compared to any other language for performing most computer language tasks, is incredibly powerful and unequalled for the tasks of building an area in MUD type games. For the beginner, it allows you to easily jump in and code useful objects without even knowing what you are doing. For the intermediate person, it allows you to turn any idea you have into textual virtual reality. And for the advanced person, it's object oriented features can allow you to build one of the most popular games on the internet. What you can do is simply limited by how much you know. And learning more does not require a computer science degree. Copyright (c) George Reese 1993 Chapter 2: The LPMud Driver 2.1 Review of Basic Driver/Mudlib Interaction In the LPC Basics textbook, you learned a lot about the way the mudlib works, specifically in relation to objects you code in order to build your realm. Not much was discussed about the interaction between the mudlib and the driver. You should know, however, that the driver does the following: 1) When an object is first loaded into memory, the driver will call create() in native muds and reset() in compat muds. A creator uses create() or reset() to give initial values to the object. 2) At an interval setup by the game administrator, the driver calls the function reset(). This allows the object to regenerate monsters and such. Notice that in a compat mud, the same function is used to set up initial values as is used to reset the room. 3) Any time a living object comes in contact with an object of any sort, the driver calls init() in the newly encountered object. This allows newly encountered objects to give living objects commands to execute through the add_action() efun, as well as perform other actions which should happen whenever a living thing encounters a given object. 4) The driver defines a set of functions known as efuns which are available to all objects in the game. Examples of commonly used efuns are: this_player(), this_object(), write(), say(), etc. 2.2 The Driver Cycle The driver is a C program which runs the game. Its basic functions are to accept connections from the outside world so people can login, interpret the LPC code which defines LPC objects and how they function in the game, and accept user input and call the appropriate LPC functions which match the event. In its most simplest essence, it is an unending loop. Once the game has booted up and is properly functioning (the boot up process will be discussed in a future, advanced LPC textbook), the driver enters a loop which does not terminate until the shutdown() efun is legally called or a bug causes the driver program to crash. First off, the driver handles any new incoming connections and passes control of the connection to a login object. After that, the driver puts together a table of commands which have been entered by users since the last cycle of the driver. After the command table is assembled, all messages scheduled to be sent to the connection from the last driver cycle are sent out to the user. At this point, the driver goes through the table of commands to be executed and executes each set of commands each object has stored there. The driver ends its cycle by calling the function heart_beat() in every object with a heart_beat() set and finally performing all pending call outs. This chapter will not deal with the handling of connections, but instead will focus on how the driver handles user commands and heartbeats and call outs. 2.3 User Commands As noted in section 1.2, the driver stores a list of commands for each user to be executed each cycle. The commands list has the name of the living object performing the command, the object which gave the living object that command, and the function which is to be executed in order to perform the command. The driver refers to the object which typed in the command as the command giver. It is the command giver which gets returned as this_player() in most cases. The driver starts at the top of the list of living objects with pending commands, and successively performs each command it typed by calling the function associated with the command and passing any arguments the command giver gave as arguments to the function. As the driver starts with the commands issued by a new living object, the command giver variable is changed to be equal to the new living object, so that during the sequence of functions initiated by that command, the efun this_player() returns the object which issued the command. Let's look at the command buffer for an example player. Since the execution of his last command, Bozo has typed "north" and "tell descartes when is the next reboot". The command "north" is associated with the function "Do_Move()" in the room Bozo is in (the command "north" is automatically setup by the set_exits() efun in that room). The command "tell" is not specifically listed as a command for the player, however, in the player object there is a function called "cmd_hook()" which is associated with the command "", which matches any possible user input. Once the driver gets down to Bozo, the command giver variable is set to the object which is Bozo. Then, seeing Bozo typed "north" and the function "north" is associated with, the driver calls Bozo's_Room- >Do_Move(0). An argument of 0 is passed to the function since Bozo only typed the command "north" with no arguments. The room naturally calls some functions it needs, all the while such that the efun this_player() returns the object which is Bozo. Eventually, the room object will call move_player() in Bozo, which in turn calls the move_object() efun. This efun is responsible for changing an object's environment. When the environment of an object changes, the commands available to it from objects in its previous environment as well as from its previous environment are removed from the object. Once that is done, the driver calls the efun init() in the new environment as well as in each object in the new environment. During each of these calls to init(), the object Bozo is still the command giver. Thus all add_action() efuns from this move will apply to Bozo. Once all those calls are done, control passes back from the move_object() efun to the move_player() lfun in Bozo. move_player() returns control back to Do_Move() in the old room, which returns 1 to signify to the driver that the command action was successful. If the Do_Move() function had returned 0 for some reason, the driver would have written "What?" (or whatever your driver's default bad command message is) to Bozo. Once the first command returns 1, the driver proceeds on to Bozo's second command, following much the same structure. Note that with "tell descartes when is the next reboot", the driver passes "descartes when is the next reboot" to the function associated with tell. That function in turn has to decide what to do with that argument. After that command returns either 1 or 0, the driver then proceeds on to the next living object with commands pending, and so on until all living objects with pending commands have had their commands performed. 2.4 The Efuns set_heart_beat() and call_out() Once all commands are performed for objects with commands pending, the driver then proceeds to call the heart_beat() function in all objects listed with the driver as having heartbeats. Whenever an object calls the efun set_heart_beat() with a non-zero argument (depending on your driver, what non-zero number may be important, but in most cases you call it with the int 1). The efun set_heart_beat() adds the object which calls set_heart_beat() to the list of objects with heartbeats. If you call it with an argument of 0, then it removes the object from the list of objects with heartbeats. The most common use for heartbeats in the mudlib is to heal players and monsters and perform combat. Once the driver has finished dealing with the command list, it goes through the heartbeat list calling heart_beat() in each object in the list. So for a player, for example, the driver will call heart_beat() in the player which will: 1) age the player 2) heal the player according to a heal rate 3) check to see if there are any hunted, hunting, or attacking objects around 4) perform an attack if step 3 returns true. 5) any other things which need to happen automatically roughly every second Note that the more objects which have heartbeats, the more processing which has to happen every cycle the mud is up. Objects with heartbeats are thus known as the major hog of CPU time on muds. <* On Kingdoms we don't have a standard heart_beat. We have different *> <* heart_beats for fighting, healing getting your sp back etc. The *> <* different_heart_beats are dependant on the player's stats and in some *> <* cases skills. - Ceril *> The call_out() efun is used to perform timed function calls which do not need to happen as often as heartbeats, or which just happen once. Call outs let you specify the function in an object you want called. The general formula for call outs is: call_out(func, time, args); The third argument specifying arguments is optional. The first argument is a string representing the name of the function to be called. The second argument is how many seconds should pass before the function gets called. <* call_outs are obsolete on Kingdoms. Instead we use set_alarm. Please *> <* look at the man file for more information. - Ceril *> Practically speaking, when an object calls call_out(), it is added to a list of objects with pending call outs with the amount of time of the call out and the name of the function to be called. Each cycle of the driver, the time is counted down until it becomes time for the function to be called. When the time comes, the driver removes the object from the list of objects with pending call outs and performs the call to the call out function, passing any special args originally specified by the call out function. If you want a to remove a pending call before it occurs, you need to use the remove_call_out() efun, passing the name of the function being called out. The driver will remove the next pending call out to that function. This means you may have some ambiguity if more than one call out is pending for the same function. In order to make a call out cyclical, you must reissue the call_out() efun in the function you called out, since the driver automatically removes the function from the call out table when a call out is performed. Example: void foo() { call_out("hello", 10); } void hello() { call_out("hello", 10); } will set up hello() to be called every 10 seconds after foo() is first called. There are several things to be careful about here. First, you must watch to make sure you do not structure your call outs to be recursive in any unintended fashion. Second, compare what a set_heart_beat() does when compared directly to what call_out() does. <* Making the alarm repetitive is done automatically within the set_alarm. *> <* Please look at the man page for details. - Ceril *> set_heart_beat(): a) Adds this_object() to a table listing objects with heartbeats. b) The function heart_beat() in this_object() gets called every single driver cycle. call_out(): a) Adds this_object(), the name of a function in this_object(), a time delay, and a set of arguments to a table listing functions with pending call outs. b) The function named is called only once, and that call comes after the specified delay. As you can see, there is a much greater memory overhead associated with call outs for part (a), yet that there is a much greater CPU overhead associated with heartbeats as shown in part (b), assuming that the delay for the call out is greater than a single driver cycle. Clearly, you do not want to be issuing 1 second call outs, for then you get the worst of both worlds. Similarly, you do not want to be having heart beats in objects that can perform the same functions with call outs of a greater duration than 1 second. I personally have heard much talk about at what point you should use a call out over a heartbeat. What I have mostly heard is that for single calls or for cycles of a duration greater than 10 seconds, it is best to use a call out. For repetitive calls of durations less than 10 seconds, you are better off using heartbeats. I do not know if this is true, but I do not think following this can do any harm. 2.5 Summary Basic to a more in depth understanding of LPC is and understanding of the way in which the driver interacts with the mudlib. You should now understand the order in which the driver performs functions, as well as a more detailed knowledge of the efuns this_player(), add_action(), and move_object() and the lfun init(). In addition to this building upon knowledge you got from the LPC Basics textbook, this chapter has introduced call outs and heartbeats and the manner in which the driver handles them. You should now have a basic understanding of call outs and heartbeats such that you can experiment with them in your realm code. Chapter 3: Complex Data Types 3.1 Simple Data Types In the textbook LPC Basics, you learned about the common, basic LPC data types: int, string, object, void. Most important you learned that many operations and functions behave differently based on the data type of the variables upon which they are operating. Some operators and functions will even give errors if you use them with the wrong data types. For example, "a"+"b" is handled much differently than 1+1. When you ass "a"+"b", you are adding "b" onto the end of "a" to get "ab". On the other hand, when you add 1+1, you do not get 11, you get 2 as you would expect. I refer to these data types as simple data types, because they atomic in that they cannot be broken down into smaller component data types. The object data type is a sort of exception, but you really cannot refer individually to the components which make it up, so I refer to it as a simple data type. This chapter introduces the concept of the complex data type, a data type which is made up of units of simple data types. LPC has two common complex data types, both kinds of arrays. First, there is the traditional array which stores values in consecutive elements accessed by a number representing which element they are stored in. Second is an associative array called a mapping. A mapping associates to values together to allow a more natural access to data. 3.2 The Values NULL and 0 Before getting fully into arrays, there first should be a full understanding of the concept of NULL versus the concept of 0. In LPC, a null value is represented by the integer 0. Although the integer 0 and NULL are often freely interchangeable, this interchangeability often leads to some great confusion when you get into the realm of complex data types. You may have even encountered such confusion while using strings. 0 represents a value which for integers means the value you add to another value yet still retain the value added. This for any addition operation on any data type, the ZERO value for that data type is the value that you can add to any other value and get the original value. Thus: A plus ZERO equals A where A is some value of a given data type and ZERO is the ZERO value for that data type. This is not any sort of official mathematical definition. There exists one, but I am not a mathematician, so I have no idea what the term is. Thus for integers, 0 is the ZERO value since 1 + 0 equals 1. NULL, on the other hand, is the absence of any value or meaning. The LPC driver will interpret NULL as an integer 0 if it can make sense of it in that context. In any context besides integer addition, A plus NULL causes an error. NULL causes an error because adding valueless fields in other data types to those data types makes no sense. Looking at this from another point of view, we can get the ZERO value for strings by knowing what added to "a" will give us "a" as a result. The answer is not 0, but instead "". With integers, interchanging NULL and 0 was acceptable since 0 represents no value with respect to the integer data type. This interchangeability is not true for other data types, since their ZERO values do not represent no value. Namely, "" represents a string of no length and is very different from 0. When you first declare any variable of any type, it has no value. Any data type except integers therefore must be initialized somehow before you perform any operation on it. Generally, initialization is done in the create() function for global variables, or at the top of the local function for local variables by assigning them some value, often the ZERO value for that data type. For example, in the following code I want to build a string with random words: string build_nonsense() { string str; int i; str = ""; /* Here str is initialized to the string ZERO value */ for(i=0; i<6; i++) { switch(random(3)+1) { case 1: str += "bing"; break; case 2: str += "borg"; break; case 3: str += "foo"; break; } if(i==5) str += ".\n"; else str += " "; } return capitalize(str); } If we had not initialized the variable str, an error would have resulted from trying to add a string to a NULL value. Instead, this code first initializes str to the ZERO value for strings, "". After that, it enters a loop which makes 6 cycles, each time randomly adding one of three possible words to the string. For all words except the last, an additional blank character is added. For the last word, a period and a return character are added. The function then exits the loop, capitalizes the nonsense string, then exits. 3.3 Arrays in LPC An array is a powerful complex data type of LPC which allows you to access multiple values through a single variable. For instance, Nightmare has an indefinite number of currencies in which players may do business. Only five of those currencies, however, can be considered hard currencies. A hard currency for the sake of this example is a currency which is readily exchangeable for any other hard currency, whereas a soft currency may only be bought, but not sold. In the bank, there is a list of hard currencies to allow bank keepers to know which currencies are in fact hard currencies. With simple data types, we would have to perform the following nasty operation for every exchange transaction: int exchange(string str) { string from, to; int amt; if(!str) return 0; if(sscanf(str, "%d %s for %s", amt, from, to) != 3) return 0; if(from != "platinum" && from != "gold" && from != "silver" && from != "electrum" && from != "copper") { notify_fail("We do not buy soft currencies!\n"); return 0; } ... } With five hard currencies, we have a rather simple example. After all it took only two lines of code to represent the if statement which filtered out bad currencies. But what if you had to check against all the names which cannot be used to make characters in the game? There might be 100 of those; would you want to write a 100 part if statement? What if you wanted to add a currency to the list of hard currencies? That means you would have to change every check in the game for hard currencies to add one more part to the if clauses. Arrays allow you simple access to groups of related data so that you do not have to deal with each individual value every time you want to perform a group operation. As a constant, an array might look like this: ({ "platinum", "gold", "silver", "electrum", "copper" }) which is an array of type string. Individual data values in arrays are called elements, or sometimes members. In code, just as constant strings are represented by surrounding them with "", constant arrays are represented by being surrounded by ({ }), with individual elements of the array being separated by a ,. You may have arrays of any LPC data type, simple or complex. Arrays made up of mixes of values are called arrays of mixed type. In most LPC drivers, you declare an array using a throw-back to C language syntax for arrays. This syntax is often confusing for LPC coders because the syntax has a meaning in C that simply does not translate into LPC. Nevertheless, if we wanted an array of type string, we would declare it in the following manner: string *arr; In other words, the data type of the elements it will contain followed by a space and an asterisk. Remember, however, that this newly declared string array hasstring array has a NULL value in it at the time of declaration. 3.4 Using Arrays You now should understand how to declare and recognize an array in code. In order to understand how they work in code, let's review the bank code, this time using arrays: string *hard_currencies; int exchange(string str) { string from, to; int amt; if(!str) return 0; if(sscanf(str, "%d %s for %s", amt, from, to) != 3) return 0; if(member_array(from, hard_currencies) == -1) { notify_fail("We do not buy soft currencies!\n"); return 0; } ... } This code assumes hard_currencies is a global variable and is initialized in create() as: hard_currencies = ({ "platinum", "gold", "electrum", "silver", "copper" }); Ideally, you would have hard currencies as a #define in a header file for all objects to use, but #define is a topic for a later chapter. Once you know what the member_array() efun does, this method certainly is much easier to read as well as is much more efficient and easier to code. In fact, you can probably guess what the member_array() efun does: It tells you if a given value is a member of the array in question. Specifically here, we want to know if the currency the player is trying to sell is an element in the hard_curencies array. What might be confusing to you is, not only does member_array() tell us if the value is an element in the array, but it in fact tells us which element of the array the value is. How does it tell you which element? It is easier to understand arrays if you think of the array variable as holding a number. In the value above, for the sake of argument, we will say that hard_currencies holds the value 179000. This value tells the driver where to look for the array hard_currencies represents. Thus, hard_currencies points to a place where the array values may be found. When someone is talking about the first element of the array, they want the element located at 179000. When the object needs the value of the second element of the array, it looks at 179000 + one value, then 179000 plus two values for the third, and so on. We can therefore access individual elements of an array by their index, which is the number of values beyond the starting point of the array we need to look to find the value. For the array hard_currencies array: "platinum" has an index of 0. "gold" has an index of 1. "electrum" has an index of 2. "silver" has an index of 3. "copper" has an index of 4. The efun member_array() thus returns the index of the element being tested if it is in the array, or -1 if it is not in the array. In order to reference an individual element in an array, you use its index number in the following manner: array_name[index_no] Example: hard_currencies[3] where hard_currencies[3] would refer to "silver". So, you now should now several ways in which arrays appear either as a whole or as individual elements. As a whole, you refer to an array variable by its name and an array constant by enclosing the array in ({ }) and separating elements by ,. Individually, you refer to array variables by the array name followed by the element's index number enclosed in [], and to array constants in the same way you would refer to simple data types of the same type as the constant. Examples: Whole arrays: variable: arr constant: ({ "platinum", "gold", "electrum", "silver", "copper" }) Individual members of arrays: variable: arr[2] constant: "electrum" You can use these means of reference to do all the things you are used to doing with other data types. You can assign values, use the values in operations, pass the values as parameters to functions, and use the values as return types. It is important to remember that when you are treating an element alone as an individual, the individual element is not itself an array (unless you are dealing with an array of arrays). In the example above, the individual elements are strings. So that: str = arr[3] + " and " + arr[1]; will create str to equal "silver and gold". Although this seems simple enough, many people new to arrays start to run into trouble when trying to add elements to an array. When you are treating an array as a whole and you wish to add a new element to it, you must do it by adding another array. Note the following example: string str1, str2; string *arr; str1 = "hi"; str2 = "bye"; /* str1 + str2 equals "hibye" */ arr = ({ str1 }) + ({ str2 }); /* arr is equal to ({ str1, str2 }) */ Before going any further, I have to note that this example gives an extremely horrible way of building an array. You should set it: arr = ({ str1, str2 }). The point of the example, however, is that you must add like types together. If you try adding an element to an array as the data type it is, you will get an error. Instead you have to treat it as an array of a single element. 3.5 Mappings One of the major advances made in LPMuds since they were created is the mapping data type. People alternately refer to them as associative arrays. Practically speaking, a mapping allows you freedom from the association of a numerical index to a value which arrays require. Instead, mappings allow you to associate values with indices which actually have meaning to you, much like a relational database. In an array of 5 elements, you access those values solely by their integer indices which cover the range 0 to 4. Imagine going back to the example of money again. Players have money of different amounts and different types. In the player object, you need a way to store the types of money that exist as well as relate them to the amount of that currency type the player has. The best way to do this with arrays would have been to store an array of strings representing money types and an array of integers representing values in the player object. This would result in CPU-eating ugly code like this: int query_money(string type) { int i; i = member_array(type, currencies); if(i>-1 && i < sizeof(amounts)) /* sizeof efun returns # of elements */ return amounts[i]; else return 0; } And that is a simple query function. Look at an add function: void add_money(string type, int amt) { string *tmp1; int * tmp2; int i, x, j, maxj; i = member_array(type, currencies); if(i >= sizeof(amounts)) /* corrupt data, we are in a bad way */ return; else if(i== -1) { currencies += ({ type }); amounts += ({ amt }); return; } else { amounts[i] += amt; if(amounts[i] < 1) { tmp1 = allocate(sizeof(currencies)-1); tmp2 = allocate(sizeof(amounts)-1); for(j=0, x =0, maxj=sizeof(tmp1); j < maxj; j++) { if(j==i) x = 1; tmp1[j] = currencies[j+x]; tmp2[j] = amounts[j+x]; } currencies = tmp1; amounts = tmp2; } } } That is really some nasty code to perform the rather simple concept of adding some money. First, we figure out if the player has any of that kind of money, and if so, which element of the currencies array it is. After that, we have to check to see that the integrity of the currency data has been maintained. If the index of the type in the currencies array is greater than the highest index of the amounts array, then we have a problem since the indices are our only way of relating the two arrays. Once we know our data is in tact, if the currency type is not currently held by the player, we simply tack on the type as a new element to the currencies array and the amount as a new element to the amounts array. Finally, if it is a currency the player currently has, we just add the amount to the corresponding index in the amounts array. If the money gets below 1, meaning having no money of that type, we want to clear the currency out of memory. Subtracting an element from an array is no simple matter. Take, for example, the result of the following: string *arr; arr = ({ "a", "b", "a" }); arr -= ({ arr[2] }); What do you think the final value of arr is? Well, it is: ({ "b", "a" }) Subtracting arr[2] from the original array does not remove the third element from the array. Instead, it subtracts the value of the third element of the array from the array. And array subtraction removes the first instance of the value from the array. Since we do not want to be forced on counting on the elements of the array as being unique, we are forced to go through some somersaults to remove the correct element from both arrays in order to maintain the correspondence of the indices in the two arrays. Mappings provide a better way. They allow you to directly associate the money type with its value. Some people think of mappings as arrays where you are not restricted to integers as indices. Truth is, mappings are an entirely different concept in storing aggregate information. Arrays force you to choose an index which is meaningful to the machine for locating the appropriate data. The indices tell the machine how many elements beyond the first value the value you desire can be found. With mappings, you choose indices which are meaningful to you without worrying about how that machine locates and stores it. You may recognize mappings in the following forms: constant values: whole: ([ index:value, index:value ]) Ex: ([ "gold":10, "silver":20 ]) element: 10 variable values: whole: map (where map is the name of a mapping variable) element: map["gold"] So now my monetary functions would look like: int query_money(string type) { return money[type]; } void add_money(string type, int amt) { if(!money[type]) money[type] = amt; else money[type] += amt; if(money[type] < 1) map_delete(money, type); /* this is for MudOS */ ...OR... money = m_delete(money, type) /* for some LPMud 3.* varieties */ ... OR... m_delete(money, type); /* for other LPMud 3.* varieties */ } Please notice first that the efuns for clearing a mapping element from the mapping vary from driver to driver. Check with your driver's documentation for the exact name an syntax of the relevant efun. As you can see immediately, you do not need to check the integrity of your data since the values which interest you are inextricably bound to one another in the mapping. Secondly, getting rid of useless values is a simple efun call rather than a tricky, CPU-eating loop. Finally, the query function is made up solely of a return instruction. You must declare and initialize any mapping before using it. Declarations look like: mapping map; Whereas common initializations look like: map = ([]); map = allocate_mapping(10) ...OR... map = m_allocate(10); map = ([ "gold": 20, "silver": 15 ]); As with other data types, there are rules defining how they work in common operations like addition and subtraction: ([ "gold":20, "silver":30 ]) + ([ "electrum":5 ]) gives: (["gold":20, "silver":30, "electrum":5]) Although my demonstration shows a continuity of order, there is in fact no guarantee of the order in which elements of mappings will stored. Equivalence tests among mappings are therefore not a good thing. 3.6 Summary Mappings and arrays can be built as complex as you need them to be. You can have an array of mappings of arrays. Such a thing would be declared like this: mapping *map_of_arrs; which might look like: ({ ([ ind1: ({ valA1, valA2}), ind2: ({valB1, valB2}) ]), ([ indX: ({valX1,valX2}) ]) })
Mappings may use any data type as an index, including objects. Mapping indices are often referred to as keys as well, a term from databases. Always keep in mind that with any non-integer data type, you must first initialize a variable before making use of it in common operations such as addition and subtraction. In spite of the ease and dynamics added to LPC coding by mappings and arrays, errors caused by failing to initialize their values can be the most maddening experience for people new to these data types. I would venture that a very high percentage of all errors people experimenting with mappings and arrays for the first time encounter are one of three error messages:
Indexing on illegal type. Illegal index. Bad argument 1 to (+ += - -=) /* insert your favourite operator */
Error messages 1 and 3 are darn near almost always caused by a failure to initialize the array or mapping in question. Error message 2 is caused generally when you are trying to use an index in an initialized array which does not exist. Also, for arrays, often people new to arrays will get error message 3 because they try to add a single element to an array by adding the initial array to the single element value instead of adding an array of the single element to the initial array. Remember, add only arrays to arrays.
At this point, you should feel comfortable enough with mappings and arrays to play with them. Expect to encounter the above error messages a lot when first playing with these. The key to success with mappings is in debugging all of these errors and seeing exactly what causes wholes in your programming which allow you to try to work with uninitialized mappings and arrays. Finally, go back through the basic room code and look at things like the set_exits() (or the equivalent on your mudlib) function. Chances are it makes use of mappings. In some instances, it will use arrays as well for compatibility with mudlib.n.
Copyright (c) George Reese 1993