This tutorial was produced in good faith for use by people who like to program muds for their own pleasure. That means that I won't charge for using or distributing it provided that this is the purpose it's being used for.
Specifically, I want to make sure that those who run commercial muds don't provide this manual as an aid to further their monetary aims. If they want it they can either pay me a lot of money for it or produce one of their own.
Please read the copyright statement in the printed section of this manual to get the full text. The gist of it, however, is exactly what I just related here.
This tutorial is intended to be something that anyone can read and learn from. Now, this is, of course, impossible, so let's amend that a bit. It's a tutorial that anyone with at least a bit of knowledge of programming and a will to learn can use. You don't have to know about C before you start, and I believe that even true virgins might be able to learn how to code. They will, of course, have a much harder job though.
Experienced coders, even mud coders, will need to read the tutorial since it explains concepts that are unique for this game, but they will be able to skim most of it and focus only on the trickier bits. I leave the selection of what is and what is not important to you, the reader, since you're the only one who knows how much you need to learn.
As the LPC language in actual application is closely knitted to the mudlib which it is used in, I will also cover part of that. However, I will not teach you how to use the mudlib in detail, you'll have to read other documents for that. All of this makes this tutorial pretty specific for games, and Genesis in particular. Keep this in mind if you are reading it on another game.
I hope you'll find the tutorial useful and worth the effort it takes to read. It sure took a lot of effort to write, so it'd better not be for nothing! :)
I'd like to start by thanking Thorsten Lockert and Christian Markus, perhaps better known as Plugh and Olorin, for their help in proofreading this text, suggesting improvements and in general being supportive and providing constructive feedback.
Without them this rather lengthy project would have taken even longer (mind-boggling, but true) to complete and ended up a lot worse.
The manual is divided into three progressively more advanced chapters. The first chapter will be explain basics about coding and LPC without delving too deep into specifics.
The second chapter will be for a more intermediate level of coding, explaining in full all the aspects of functions and operators that might have been treated a bit too easy in the first chapter.
The third and final chapter handles whatever might be left when chapter two is done. Well, not everything; the tutorial will not explain all the intricacies of the gamedriver and the mudlib. If you are looking for info about creating your own mudlib or doing very advanced stuff you'll have to learn that from reading the actual code.
If you are a newbie wizard you might feel a bit taken back at first, looking at this rather thick tutorial. However, it's quite necessary that you read all of it and leave nothing for the future. You will undoubtably have to at least recognize subjects from all three chapters, even though mostly you will actually only use the information in the first two. Heck, there's a lot of old wizards out there who hardly even master the first one, a scary thought! Among other things that's one of the fundamental reasons why I'm writing this manual...
The manual is fairly extensive, but it's for learning LPC for domain coding only. This means that it's not a complete LPC reference manual. Some of the more esoteric and unusual efuns and functionalities are not covered since they only would be used by mudlib coders or gamedriver hackers. This manual is not intended for them. Sorry, if that's what you were looking for you'll have to keep on searching for another source.
A small note about gender. Throughout the text I've used male pronouns. This is not because I scoff the thought of female coders, it's because the English language is male-oriented. Fact of life, like it or not. I guess I could have added a `(or she)' comment after all occurrences of the male `he', but that strikes me as more than just a bit silly. Until the English language comes up with a strictly neutral pronoun I'll stick to using the all-inclusive male one.
Chapters that describe efuns/sfuns/lfuns/macros in detail have a subcaption with the names of the discussed items within brackets for easy finding if you're just browsing the tutorial later.
LPC is the interpreted mud language created by Lars Pensjoe for his invention LPMUD, an interactive multiuser environment suited for many purposes, games not the least among them. Since the first appearance in 1988 the language has changed dramatically.
Development was taken over by other people at Chalmers Datorfoerening, majorly Jakob Hallen, Lennart Augustsson and Anders Chrigstroem around 1990. They extended and refined the language extensively, but, as the name LPC hints of, it still retains its links to the language 'C'. The main differences lie in the object structure that's imposed on the language as well as some new data types to faciliate game programming. The language is not as free-form as 'C', but on the other hand it's more suitable for the purpose which it was created for - programming in a game environment.
The distinction between gamedriver and mudlib might seem hard to define at times, but it's really very simple. The gamedriver is the program that runs on the host computer. It is basically an interpreter in conjunction with an object management kernel, almost a small operating system in its own right. It defines and understands the LPC language, interprets it and executes the instructions given to it through the LPC objects in the game.
The mudlib is the collection of LPC objects that make up the basic game environment. While the gamedriver knows nothing about what it actually is doing, the mudlib does. (The mudlib can conversly be said to have no idea about how it does what it does, while the gamedriver does). It contains the basic set of LPC objects that are used in the game, the players, the monsters, the rooms etc. Individual efforts (domain code) are stored and handled separately and uses both the services of the gamedriver and the mudlib.
The game can be said to have been divided into three distinctive parts as suggested above; The gamedriver, the mudlib and the domain code. The gamedriver and mudlib I have already explained. The domain concept is a way to organize the way code is written. A domain is principally a group of wizards working towards a predefined goal. This project can be limited in space, an actual area in the game world, or as intangiable as a guild or sect that the players can become members of.
Within a domain there is a leader, a domain Lord. He is the local taskmaster who decides what goes on and in which direction work should progress. In the domain all code is shared easily and usually is strongly interconnected.
Naturally there have to be ties between different domains as well, but these are usually weaker and actual code is seldom shared.
As a newbie wizard you will try to find a domain in which to enroll, that sounds interesting and inspiring to work with.
It might seem premature to tell you what your code should look like before you have learnt how to write it. However, it is a fundamental subject of great importance. Someone jokingly said that writing code correctly will make your teeth whiter, your hair darker and improve your sexlife. Well... it might not do that, but it'll certainly improve the overall quality of what you produce, at a very low cost. Mainly it's a matter of self-discipline.
Here are some good arguments for making the effort:
What follows here is a rather lengthy instruction of how to put down your code in writing. Read it now even though you might not fully understand what is being discussed, then go back and re-read it later after having learnt the skills necessary. Doing that will make sure you'll remember the correct way of formatting your code.
(,
if any.while (test)
statement;
if (this)
{
statement;
}
else if (that)
{
another_statement;
}
else
{
default_statement;
}
Now, this is almost a religious matter for some coders. Representatives of
another sect preaches that the block opening brace should be on the end of
the line of the block statement and how you do this really isn't that
important. Not as long as you do it the way I say, or you'll burn in COBOL
hell forever :) No, seriously, pick one of the two ways of doing it and
stick to it. The only real important thing is that you keep the
indention-level straight throughout the code. If we all agree on something,
it's that wavy indention-levels is something not to be tolerated.
;-separated lists and binary operators have a space
both in front of, and after the operator.int a, b, c;
for (a = 0 ; a < 10 ; a++)
{
b = function_call(a, b * 2);
c = b * 3 / 4;
}
; on a
separate line.while (!(var = func(var)))
;
The reason for this is that if you should put it on the same line, it's very
easy to miss real mistakes like this one just out of pure laziness:for (i = 0 ; i < 100 ; i++);
{
<code that gets executed only once, but always>
}
#define and #include statements should be
placed at the top of the file. It's possible to spread them out, but that
will just be confusing.
public void
my_function(int a, int b)
{
< code >
}
/* * <filename> * * <Short description of what the file does, no more than 5-7 lines. * ... * ... > * * Copyright (C): <your name and year> * */Read the game Copyright statement NOW in order to know what rules apply to code you produce for the game, in the game. It ought to reside in the file `/doc/COPYRIGHT'. If not, simply ask one of the game administrators.
/* * Function name: <Function name> * Description: <Short description of what the function does, * usually no more than three lines. * Arguments: <A list of all arguments, one per line * arg1 - description no longer than the line. * arg2 - next argument, etc. > * Returns: <What the function returns> */If the function doesn't take any arguments, or doesn't return anything, simply remove those lines in the header.
/* * Comment for code following this comment, * telling what it does */ < code >
function_name()).
Global variables should be written using the first letter of the word
capitalized (e.g. int GlobalTime;). #defines
should be written in capitals (e.g. #define AMOEBA "one celled
creature"). Doing this makes it easy to see what kind of symbol
is being handled at all times.Now, the easiest way of getting the basic stuff done properly is to use the
emacs editor, set up to use a modified c++ mode. The c++ mode understands about ::-operators
but needs a few hints on tabstops etc. Put these lines of code into your .emacs
file and all will work just as it should:
;; emacs lisp script start
(setq auto-mode-alist (append '(
("\\.l" . my-c++-mode)
("\\.y" . my-c++-mode)
("\\.c" . my-c++-mode)
("\\.h" . my-c++-mode))
auto-mode-alist))
(defun my-c++-mode () (interactive)
(c++-mode)
(setq c-indent-level 4)
(setq c-brace-offset 0)
(setq c-label-offset -4)
(setq c-continued-brace-offset -4)
(setq c-continued-statement-offset 4))
;; emacs end
An added advantage of using emacs is that later, when debugging other coder's attempts at writing code, correcting their horrible indention is as easy as typing 'M-<', 'M->', 'M-x indent-region'.
This chapter will teach you the very basics of programming, essential for understanding what follows. It will also introduce you to the concept of object oriented programming and explain some of the mudlib.
We begin with he basic programming principles and the structure of LPC and the LPC environment.
This is a very philosophical question really. However, lets stick to the practical side and leave the zen bit to those who go for that kind of stuff.
Programming basically is the art of identifying a problem and putting the solution into symbols a computer can understand. A good programmer has a highly developed ability to see how a given problem can be split into smaller problems for which he has several solutions, and he also knows which particular solution he should pick to make the result as effective in terms of memory and speed as possible.
A programmer, as the previous passage suggests, actually tells the computer how to solve a problem. A computer can not come up with a solution to a problem itself. However, it's a lot quicker than you are, so problems that you can solve but would take you several lifetimes or simply just 'too long' are handled quickly by the computer.
What you need to learn is that special way of thinking that allows you to do this 'subdividing' thing where you see the steps needed to get from the beginning state to the solved state. You also need to learn the methods that make up these steps. Naturally this tutorial won't teach you 'how to program', it will only teach you the language used to put down the program in.
Who'll teach you programming then, in case you don't already know how to? Well... primarily other wizards in the game, and then yourself. Hard work in other words, there's never any shortcuts unfortunately, no matter what you need to learn. However, since this is a very amusing game let's hope you'll have great fun while acquiring the skills.
Programs are nothing but files containing instructions suited for the computer to understand. To program is to write lists of instructions in such a way that the computer reaches a predefined goal. Usually a program is compiled - translated - into a low-level code expressed in binary symbols (high and low electrical states in computer memory) which the computer understands with ease. The actual language you use to program in is merely a convenient go-between, a compromise which both you and the computer can understand. The reason you compile code is that the translation step is fairly complicated and time-consuming. You'd rather just do that once and then store the result, using it directly over and over again.
LPC however, isn't compiled, it's interpreted. The instructions are read and translated to the computer one by one, executed and forgotten. Well, this is not 100% true. In fact, the gamedriver which is the program running on the host computer translates the LPC code into an intermediate simple instruction code. This set of instruction codes makes up the code part of the so called 'master object' held in computer memory. When you run an LPC program, the instruction set is traced, line by line as described above, causing the computer to execute a predefined set of actions defined by the instructions.
The difference between having interpreted and compiled code is that while the compiled code is quicker, the interpreted version is much esier to modify. If you want to change something in the compiled version you have to make the change in the source code, then recompile and store the new version, then try it out. With interpreted code you just make the change in the source and run it. With LPC you need to instruct the gamedriver to destroy the old master object instruction set as well, but more about that later.
LPC programs are described above as files containing instructions to the computer written in the language LPC. These files must be named something ending in the letters `.c' (e.g. `test.c') so that the gamedriver knows what it's dealing with; an LPC program. Program names can be any string of printable characters < 32 characters in length, beginning with an alphabetical letter. However, in practice it is recommendable that you limit yourself to < 16 letter strings that are made up of ordinary lowercase alphabetical letters only. If you want the name to be made up by two words, separate them with the `_'- character (e.g. `my_file_name.c').
An object in LPC is simply a copy of an existing and loaded program in computer memory. When a program is loaded into memory to produce a master object, the code is compiled to produce the instruction list described earlier and a chunk of memory is associated to it as specified by the program code for use for internal global variables (described later). When a copy, a clone of this program is made, a special reference called an object pointer is created. That pointer is given a reference to the master code instruction list and a unique chunk of memory. If you clone the object another time, a new pointer is created and a new chunk of memory allocated. When an object is destroyed its associated memory is freed for use by other objects, but the instruction list is kept untouched. An object is always cloned from the master object. If you want to change the program you must update the master object to instruct the gamedriver that a new list of instructions is to be compiled from the source code.
However, any already existing clones will not change just because the master does. They will keep their reference to the old instruction list. It's important that you remember this so that you don't believe that the behaviour of an old cloned object changes just because you have updated the master object. As you see it's possible to have clones of objects in the game that behave differently, simply because they are made out of different source codes, just cloned between updates and changes in code. This could be a great source of confusion, so keep it in mind.
An object is comprised of something called functions and variables. A function is a set of instructions you can reference by a name. A variable is a kind of container you can store data in for use by the functions. Some functions are already defined by the gamedriver, they are called external functions, or efuns. Functions defined in LPC code are called local functions, or lfuns. To confuse matters further there is a set of functions that count as efuns, but are written in LPC. These functions are called simulated efuns or sfuns.
An efun basically is a function that is impossible to create in LPC. Take for
example the function write() that allows you to present text on the
screen of a player. It's impossible to make that up from other functions in LPC,
so it has to be in the gamedriver. This efun is available in all LPC
programs. Efuns also have no idea about what environment they are used in. They
don't care one bit if they are used to simulate strawberry tasting, or as part
of a game.
A function like add_exit() on the other hand, that adds an exit
in a room is only available in room type objects. It is written in LPC. The
lfuns as a rule are part of the makeup of the environment in which the objects
are used. The example add_exit() for instance is well aware of such
ideas as directions and travel costs, a very limited and specific concept.
The function creator() is a good example of the third case. It's
a function that is available in every object, it returns information about who
created a certain object. This information is very specific to the
environment since it deals with such notions as code localization. This kind of
function is easy to write in LPC but on the other hand it must be available in
all objects, as if it was an efun. Due to this fact the special object `/secure/simul_efun.c'
is made to be automatically available from all other objects in the game, you'll
find all sfuns in there. This functionality is perfectly transparent to you; you
just use them as you use any other efun and you don't have to be aware of that
it really is an sfun.
LPC if fairly similar to the language C, although some differences exist. As the experienced coder will find, it basically is a bit simplified with some new convenient types added and a set of functions to handle those types. Some inconsistencies exist but they are not serious enough to cause any problems as long as you are aware of them.
It might sound strange that I start off with this, but there'll be comments everywhere, so you need to be able to recognize them from the very start.
There are two kinds of comments:
<code> // This is a comment stretching to the end of the line. <code> /* This is an enclosed comment */ <code>
As you can see, the first type of comment starts off with the //
characters and then stretches all the way to the end of the line. If you want
more lines of comments, you'll have to start off those as well with new //
characters.
The second type is a type that has a definite length. They start with /*
and end with */. This kind of comment is useful when you have to
write something that will stretch over several lines, as you only have to write
the comment symbol in the start and the beginning.
NB! The /* */ comment can not be
nested. I.e. you can not write something like this for example:
/* A comment /* A nested comment */ the first continues */
What will happen is that the comment will end with the first found */,
leaving the text the first continues */ to be interpreted as if it
was LPC code. Naturally this won't work and instead you'll get an error message.
The object holds information in variables. As the name hints these are labeled containers that may hold information that varies from time to time. It processes information in functions that both use and return data of various kinds.
In principle only one kind of data type is needed, a sort of general container that would cover anything you wanted to do. In reality it's much preferred if you can distinguish between different types of information. This might seem only to add to your programming problems, but in fact it reduces the risk of faulty code and improves legibility. It much improves on the time it takes to code and debug an object.
In LPC it is possible to use only that 'general purpose' data type I was talking about before. In the first versions of the language it was the only kind available. However, with the LPC we have today it is much preferrable if you avoid that as much as you can. In fact, start all your programs with the following instruction on a single line:
#pragma strict_types
This is an instruction to the gamedriver to check all functions so that they conform to the situation they are used in, and cause compile errors otherwise. This is a very great help in detecting programming errors early so that you don't wonder what's going on later when things don't quite turn out the way you wanted.
Now, the following types of data types are defined:
3, 17, -32, 999.
1.3,
-348.4, 4.53e+4. The range values are approximate since this might
vary from mud to mud as it's platform dependant. If there are any FORTRAN fossils
around there, beware that numbers like 1. or .4711
are not recognized as floats, you have to specify both an integer
and a decimal part, even if they only are 0.
"x",
"the string", "Another long string with the number 5 in
it". Strings can contain special characters like newline ("\n")
to end a line. A lot of LPC expressions can handle strings directly, unlike
usual C. This makes strings very handy and easy to use.
([
"Olle":23, "Peter":54, "Anna":15 ]). As
you can see the value to the right has been associated to the value to the
left. You can then extract the associated value through a simple indexing
operation using the left hand value as index.
*
in front of the variable name in the declaration. Arrays in LPC are more
like lists than proper arrays. A number of functions and operator facilities
exist to make them easy and quick to use.
A variable is a string of letters identifying an information 'box', a place to store data. The box is given a name consisting of < 32 characers, starting with an alphabetic letter. Custom and common sense dictate that all variables used inside a function consist of lowercase letters only. Global variables have the first letter in uppercase, the rest lowercase. No special character other than the '_' used to separate words is ever used. Variables should always be given names that reflect on their use. You declare variables like this:
<data type> <variable name>, <another variable>, ..., <last variable>;
e.g.
int counter;
float height, weight;
mapping age_map;
Variables must be declared at the beginning of a block (right after the first '{') and before any code statements. Global variables, variables that are available in all functions througout the program, should be declared at the top of the file.
When the declarations are executed as the program runs, they are initially
set to 0, NOT to their 'null-state' values. In other words for
example mappings, arrays and strings will all be set to 0 and not to ([]),
({}) and "" as you might believe. It is
possible to initialize variables in the declaration statement, and it's even a
very good habit always to initialize arrays and mappings there:
<data type> <variable name> = <value>, etc.
e.g.
int counter = 8;
float height = 3.0, weight = 1.2;
mapping age_map = ([]);
object *monsters = ({});
The reason why arrays and mappings should be initalized in the declaration
statement to their 'NULL' values (({}) and ([])
respectively) is that otherwise they are initialized to 0, which is incompatible
with the proper type of the variable and might cause problems later.
A function must give proper notification of what kind of data type it returns, if any. A function is a label much like a variable name, consisting of < 32 characters, starting with a letter. Custom and common sense dictate that all function names should be lowercase and only contain the special character '_' to separate words. Use function names that clearly reflect on what they do. A function declaration looks like this:
/*
* Function name: <Function name>
* Description: <What it does >
* Arguments:
* Returns: <What the function returns>
*/
<return type>
<function name>(<argument list>)
{
<code expressions>
}
/*
* Function name: compute_diam
* Description: Compute the diameter of a circle given the
* circumference.
* Variables: surf_area - the surface area
* name - the name given the circle
* Returns: The circumference.
*/
float
compute_diam(float surf_area, string name)
{
float rval;
// Circumference = pie * diameter
rval = surf_area / 3.141592643;
write("The diameter of " + name + " is " + ftoa(rval) + "\n");
return rval;
}
The argument list is a comma-separated list of data types, much like a variable declaration where you specify what kind of data will be sent to the function and assign names to this data for later use in the function. The data recieved will only be usable inside the function, unless you explicitly send it out through a function call.
(In order to save space and improve on legibility in the manual I won't put a header to all my short example functions).
A function that doesn't return anything should be declared as void.
void
write_all(string mess)
{
users()->catch_msg(mess);
}
We need to define what a statement and what an expression is in order to be able to proceed.
A statement is sort of a full sentence of instructions, made up from one or
more expressions. Statements usually cover no more than a single line of code.
Sometimes it's necessary to break it up though if it becomes too long, simply to
improve on legibility. For most statements you simply break the line between two
words, but if you are in the middle of a string you need to add a backslash (\)
at the end of the line in order for the gamedriver to understand what's going
on.
write("This is an example of \
a broken string.\n");
However, breaking a statement with backslash is extremely ugly and makes the
code hard to read. In fact, it's usually possible to break the line naturally at
the end of a string, between two operators of some kind, or even just split the
string in half and add the two parts together with the + operator.
The only time the backslash really is necessary is in #define-statements,
handled later.
write("This is a better way of " +
"breaking a string.\n");
Statements in LPC are usually ended with a ;, which also is a
good place to end the line. There's nothing stopping you from entering another
statement right after, other than that it will look awful.
An expression is an instruction or set of instructions that results in a
value of some kind. Take +, for example. It uses two other
expressions to make up a result. A variable is an expression since it yields its
contents as a result. The combination of the following two expressions and an
operator is a valid expression: a + b, a and b
being variables (expressions) and + being the operator used on
them. a = b + c; is a full statement ending in a ;.
Function calls are valid expressions. They are written simply as the name
followed by a set of matched parentheses with the arguments that the functions
uses listed inside. Take the simple function max() for example,
that returns the max of the two arguments. To determine the maximum of 4
and 10, you would write max(4, 10) as the expression.
Naturally the result must be either stored or used.
There are a lot of statements, for example conditional statements, that in
certain circumstances execute one specified statement and never else.
Suppose you want to have several statements executed and not just one? Well,
there's a special statement called block statement that will
allow you to do that. A block is defined as starting with a { and
ending with a }. Within that block you may have as many statements
of any kind (including variable definitions) as you like. The block statement is
not ending with a ;, even though it doesn't matter if you
accidentally put one there.
As stated ; is mostly used to terminate statements, however it's
also a statement in its own right.
The ; on it's own will simply be a null-statement causing
nothing to happen. This is useful when you have test-clauses and loops
(described later) that perform their intended purpose within the test or loop
clause and aren't actually intended to do anything else.
Scope is a term defining where a function or variable declaration is valid. Since programs are read top down, left right (just like you read this page), declarations of functions and variables are available to the right and below of the actual declaration. However, the scope might be limited.
A variable that is declared inside a function is only valid until the block
terminator (the terminating }) for that variable is reached.
< top of file >
int GlobCount;
// Only GlobCount is available here
void
var_func(int arg)
{
int var_1;
// GlobCount, arg and var_1 is available here
< code >
{
string var_2;
// GlobCount, arg, var_1 and var_2 is available in this block
< code >
}
// GlobCount, arg and var_1 is available here
< code >
{
int var_2;
mapping var_3;
// GlobCount, arg, var_1, var_2 and var_3 is available here
// NB this var_2 is a NEW var_2, declared here
< code >
}
// GlobCount, arg and var_1 is available here
< code >
}
// Here only GlobCount (and the function var_func) is available
Function declarations follow the same rule, though you can't declare a function inside another function. However, suppose you have these two functions where the first uses the second:
int
func_1()
{
< code >
func_2("test");
}
void
func_2(string data)
{
< code >
}
Then you have a problem, because the first function tries to use the second
before it is declared. This will result in an error message if you have
instructed the gamedriver to require types to match by specifying pragma
strict_types as suggested earlier. To take care of this you can either
re-arrange the functions so that func_2 comes before func_1
in the listing, but this might not always be possible and the layout might
suffer. Better then is to write a function prototype. The
function prototype should be placed in the top of the file after the inherit
and #include statements (described later) but before any
code and look exactly as the function declaration itself. In this case:
< top of file,inheritand#includestatements > void func_2(string data); < the actual code >
The LPC language defines a large set of operators expressions, simply expressions that operate on other expressions. What follows here is a list of them. I've used a condensed notation so that the text won't take all of the page before getting down to actual explanations.
The statement 'a = 1, 2, 3;' will set 'a' to contain '1'.
'a = b = 4;' will set a and b to be 4. It can also be written 'a = (b = 4)' to illustrate the order of execution.
'14 % 3' will yield 2 as the remainder. '14 / 3' will be 4, and 4 * 3 + 2 = 14 as a small check.
'a = 3; b = ++a;' will yield the result 'a = 4, b = 4', while 'a = 3; b = a++;' will yield the result 'a = 4, b = 3'.This only works on integers.
'a = 3; b = --a;' will yield the result 'a = 2, b = 2', while 'a = 3; b = a--;' will yield the result 'a = 2, b = 3'.This only works on integers.
Boolean (binary) operators are applicable only to integers with the exception
of the & operator which also works on arrays. Internally an
integer is 32 bits long. However, in the following examples I will only show the
ten last bits as the others are 0 and can be ignored with the one exception of
the ~-operator.
1011101001 (= 745) 1000100010 & (= 546) ------------ 1000100000 (= 544) => 745 & 546 = 544Used on two arrays, this function will return a new array that holds all elements that are members of both of the argument arrays.
1011101001 (= 745) 1000100010 | (= 546) ------------ 1011101011 (= 747) => 745 | 546 = 747
1011101001 (= 745) 1000100010 ^ (= 546) ------------ 0011001011 (= 203) => 745 ^ 546 = 203
00000000000000000000001011101001 ~ (= 745) ---------------------------------- 11111111111111111111110100010110 (= -746) => ~745 = -746NB! The above example might be hard to understand unless you really know your binary arithmetic. However, trust me when I say that this is not a typo, it's the way it should look. Read a book on boolean algebra (the section on two-complement binary arithmentic) and all will be clear.
5 << 4 => 101(b) << 4 = 1010000(b) = 80
1054 >> 5 => 10000011110(b) >> 5 = 100000(b) = 32
All of the arithmetic and boolean operator expressions can be written in a shorter way if all you want to do is compute one variable with any other expression and then store the result in the variable again.
Say that what you want to do is this a = a + 5;, a much neater
way of writing that is a += 5;. The value of the second expression
is added to the first and then stored in the first which happens to be a
variable.
You write all the others in the same way, i.e. the variable, then the
operator directly followed by = and then the expression.
a >>= 5; // a = a >> 5; b %= d + 4; // b = b % (d + 4); c ^= 44 & q; // c = c ^ (44 & q);
The table below summarizes the rules for precedence and associability of all
operators, including those which we have not yet discussed. Operators on the
same line have the same precedence, rows are in order of decreasing precedence,
so, for example, *, / and % all have the
same precedence, which is higher than that of + and -.
Note that the precedence of the bit wise logical operators &,
^ and | falls below == and !=.
This implies that bit-testing expressions like
if ((x & MASK) == 0) ...
must be fully parenthesized to give proper results.
Conditional statements are used a lot in LPC, and there is several ways of
writing them. A very important concept is that 0 is considered as false
and any other value as true in tests. This means that empty
listings ({}), empty strings "" and empty
mappings ([]) also are evaluated as true since
they aren't 0. You have to use special functions to compute their
size or determine content if you want test them, more about that later however.
The most common conditional statement is naturally the if
statement. It's easy to use and can be combined with an else clause
to handle failed tests. It's written like this:
if (expression) statement;
e.g.
if (a == 5)
a -= 4;
If you want to handle the failed match, you add an else statement like this:
if (expression) true-statement else false-statement;
e.g.
if (a == 5)
a -= 4;
else
a += 18;
If one variable has to be tested for a lot of different values, the resulting list of `if-else-if-else' statements soon gets very long and not very easy to read. However, if the type of the value you are testing is an integer, a float or a string you can use a much denser and neater way of coding. Assume you have the following code you want to write:
if (name == "fatty")
{
nat = "se";
desc = "blimp";
}
else if (name == "plugh")
{
nat = "no";
desc = "warlock";
}
else if (name == "olorin")
{
nat = "de";
desc = "bloodshot";
}
else
{
nat = "x";
desc = "unknown";
}
The better way of writing this is as follows:
switch (name)
{
case "fatty":
nat = "se";
desc = "blimp";
break;
case "plugh":
nat = "no";
desc = "warlock";
break;
case "olorin":
nat = "de";
desc = "bloodshot";
break;
default:
nat = "x";
desc = "unknown";
}
The workings of this statement is very simple really: switch
sets up the expression value within the parenthesis for matching. Then every
expression following a case is examined to find a match.
NB! The case expression must
be a constant value, it can't be a variable, function call or other type of
expression.
After a match has been found the following statements are executed until a break
statement is found. If no matching value can be found, the default
statements are executed instead.
NB! While it's not mandatory to have a default
section, it's highly recommended since that usually means that something has
happened that wasn't predicted when writing the program. If you have written it
that way on purpose that's one thing, but if you expect only a certain range of
values and another one turns up it's usually very good to have an error message
there to notify the user that something unexpected happened.
If you forget to put in a 'break' statement the following 'case' expression will be executed. This might sound like something you don't want, but if in the example above the names `fatty' and `plugh' both should generate the same result you could write:
case "fatty":
/* FALLTHROUGH */
case "plugh":
< code >
break;
... and save a bit of space. Writing code with switch doesn't make it any
quicker to execute, but a lot easier to read thereby reducing the chance of
making mistakes while coding. Remember to put the /* FALLTHROUGH */
comment there though, or it might be hard to remember later if it was
intentional or an omission of a break statement, particularly if
you have some code that's executed previously to the fallthrough. A good idea is
usually to add an extra linefeed after a break statement just to
give some extra 'breathing space' to improve on legibility.
This is a very condensed way of writing an if/else statement and
return a value depending on how the test turned out. This isn't a statement
naturally, it's an expression since it returns a value, but it was hard to
explain earlier before explaining the if/else statement.
Suppose you want to write the following:
if (test_expression)
var = if_expression;
else
var = else_expression;
You can write that much more condensed in this way:
var = test_expression ? if_expression : else_expression;
e.g.
name = day == 2 ? "tuesday" : "another day";
It can be debated if writing code this way makes you code easier or harder to read. As a rule it can be argued rather successfully that one expression of that kind does make it clearer, but that a combination of several only makes it worse. Something like this definately isn't an improvement:
name = day == 2 ? time == 18 ? "miller time" : "tuesday" : "another day";
Basically there are two kinds of loop statements which incorporate the use of conditional statements within them, i.e. they can be programmed to execute only until a certain state is reached.
If you want a simple counter you should use the for statement. The syntax is as follows:
for (initalize_statement ; test_expression ; end_of_loop_statement)
body_statement;
When first entered, the for statement executes the initialize_statement
part. This part usually is used to initialize counters or values used during the
actual loop. Then the actual loop starts. Every loop starts by executing the test_expression
and examining the result. This is a truth conditional, so any answer not equal
to 0 will cause the loop to be run. If the answer is true the body_statement
is executed, immediately followed by the end_of_loop_statement.
In the body_statement you usually do what you want to have done
during the loop, in the end_of_loop_statement you usually
increment or decrement counters as needed.
Throughout the previous section I used the word usually a lot. This is because you don't have to do it that way, there's no rule forcing you to make use of the statements in the way I said. However, for now let's stick to the regular way of using the for-statement. Later on I'll describe more refined techniques.
Assume you want to compute the sum of all integers from 7 to 123 and don't know the formula ((x2^2 + x1^2) / 2). The easiest (if not most efficient) way of doing that is a loop.
result = 0;
for (count = 7 ; count < 124 ; count++)
result += count;
What happens is that first of all result is set to 0, then the actual for-statement
is entered. It begins by setting the variable count to 7. Then the loop is
entered, beginning by testing if count (= 7) is less than 124, it is so the
value is added to count. Then count is incremented one step and the loop entered
again. This goes on until the count value reaches 124. Since that isn't less
than 124 the loop is ended.
NB! The value of count after the for-statement
will be 124 and not 123 that some people tend to believe. The test_expression
must evaluate to false in order for the loop to end, and in this
case the value for count then must be 124.
The while statement is pretty straight-forward, you can
guess from the very name what it does. The statement will perform another
statement over and over until a given while expression returns
false. The syntax is simple:
while (<test expression>)
Note carefully that the test expression is checked first of all, before running the statement the first time. If it evaluates as false the first time, the body is never executed.
a = 0;
while (a != 4)
{
a += 5;
a /= 2;
}
Sometimes during the execution of switch, for or while
statements it becomes necessary to abort execution of the block code, and
continue execution outside. To do that you use the break statement.
It simply aborts execution of that block and continues outside it.
while (end_condition < 9999)
{
// If the time() function returns 29449494, abort execution
if (time() == 29449494)
break;
< code >
}
// Continue here both after a break or when the full loop is done.
< code >
Sometimes you merely want to start over from the top of the loop you are
running, in a for or while statement, that's when you
use the continue statement.
// Add all even numbers
sum = 0;
for (i = 0 ; i < 10000 ; i++)
{
// Start from the top of the loop if 'i' is an odd number
if (i % 2)
continue;
sum += i;
}
It's time to dig deeper into the special type array and mapping. Their use might look similar but in fact they are very different from each other as you will see.
To both of these data types there exists a number of useful (indeed even essential) efuns that manipulates them and extracts information from them. They will be described in total later however, only some are mentioned here.
Arrays really aren't arrays in the proper sense of the word. They can better be seen as lists with fixed order. The difference might seem slight, but it makes sense to the computer-science buffs :)
Arrays are type-specific. This means that an array of a certain type only can
contain variables of that single type. Another restrictions is that all arrays
are one-dimensional. You can't have an array of arrays. However, the mixed
type takes care of these limitations. A mixed variable can act as an array
containing any data type, even other arrays. As a rule you should try to use
properly typed arrays to minimize the probabilities of programming mistakes
however.
You declare an array like this:
<type> *<array name>;
e.g.
int *my_arr, *your_arr;
float *another_arr;
object *ob_arr;
The initial values of these declared arrays is '0', not an empty array. I repeat: they are initialized to 0 and not to an empty array. Keep this in mind!
You can allocate and initialize an array like this:
<array> = ({ elem1, elem2, elem3, ..., elemN });
e.g.
my_arr = ({ 1, 383, 5, 391, -4, 6 });
You access members of the array using brackets on the variable name. (Assume val here is declared to be an integer).
<data variable> = <array>[<index>];
e.g.
val = my_arr[3];
LPC, like C, starts counting from 0, making the index to the fourth value = 3.
To set the value of an existing position to a new value, simply set it using
the = operator.
my_arr[3] = 22; // => ({ 1, 383, 5, 22, -4, 6 })
my_arr[3] = 391; // => ({ 1, 383, 5, 391, -4, 6 })
If you want to make a subset of an array you can specify a range of indices within the brackets.
<array variable> = <array>[<start_range>..<end_range>];
e.g.
your_arr = my_arr[1..3];
... will result in your_arr becoming the new array ({ 383,
5, 391 }); If you give a new value to an old array, the previous array is
lost.
e.g.
my_arr = ({ });
... will result in my_arr holding an empty array. The old array
is deallocated and the memory previously used is reclaimed by the gamedriver.
If you index outside an array, an error occurs and execution of the object is aborted. However, range indexing outside the array does not result in an error, the range is then only constrained to fall within the array.
If you want to create an empty array, initialized to 0 (no matter the type of
the array, all positions will be set to 0 anyway) of a given length, you use the
efun allocate().
<array> = allocate(<length>);
e.g.
your_arr = allocate(3); // => your_arr = ({ 0, 0, 0 });
Concatenating (adding) arrays to each other is most easily done with the +
operator. Simply add them as you would numbers. The += operator
works fine as well.
my_arr = ({ 9, 3 }) + ({ 5, 10, 3 }); // => ({ 9, 3, 5, 10, 3 })
Removing elements from an array is easiest done with the -/-=
operator, however, be aware that it is a general operator that will remove all
items found that match the item you want to remove.
my_arr -= ({ 3, 10 }); // => ({ 9, 5 })
If you want to remove a single item in the middle somewhere that might have been repeated, you have to use the range operator of course.
my_arr = ({ 9, 3, 5, 10, 3 });
my_arr = my_arr[0..0] + my_arr[2..4]; // => ({ 9, 5, 10, 3 })
NB! Beware this difference!!!! One is a list, the other an integer!
<array> my_arr[0..0] // = ({ 9 })
<int> my_arr[0] // = 9
Mappings are lists of associated values. They are mixed by
default, meaning that the index part of the associated values doesn't have to be
of the same type all the time, even though this is encouraged for the same
reason as before in regard to the mixed data type.
Mappings can use any kind of data type both as index and value. The index part of the mapping in a single mapping must consist of unique values. There can not be two indices of the same value.
This all sounds pretty complicated, but in reality it's pretty simple to use. However, it will be a lot easier to understand once we get down to actually seeing it used.
You declare a mapping just like any other variable, so let's just start up with a few declarations for later use:
mapping my_map; int value;
Allocating and initializing can be done in three different ways:
1: <mapping_var> = ([ <index1>:<value1>, <index2>:<value2>, ... ]); 2: <mapping_var>[<index>] = value; 3: <mapping_var> = mkmapping(<list of indices>, <list of values>);
The first is straight-forward and easy.
1: my_map = ([ "adam":5, "bertil":8, "cecar":-4 ]);
The second works so that in case a given mapping pair doesn't exist, it is created when referenced. If it does exist the value part is replaced.
2: my_map["adam"] = 1; // Creates the pair "adam":1 my_map["bertil"] = 8; // Creates the pair "bertil":8 my_map["adam"] = 5; // Replaces the old value in "adam" with 5. ...
The third requires two arrays, one containing the indices and one containing the values. How to create arrays was described in the previous chapter.
3: my_map = mkmapping(({ "adam", "bertil", "cecar" }), ({ 5, 8, -4 }));
Unlike arrays there's no order in a mapping. The values are stashed in a way that makes finding the values as quick as possible. There are functions that will allow you to get the component lists (the indices or values) from a mapping but keep in mind that they can be in any order and are not guaranteed to remain the same from call to call. In practice though, they only change order when you add or remove an element.
Merging mappings can be done with the +/+= operator just as with
mappings.
my_map += ([ "david":5, "erik":33 ]);
Removing items in a mapping, however, is a bit trickier. That has to be done
by using the special efun m_delete() (also described later).
my_map = m_delete(my_map, "bertil"); my_map = m_delete(my_map, "david");
As you see the mapping pairs has to be removed one by one using the index as an identifier of which pair you want to remove. Another thing you now realize quite clearly is that the indices in a mapping has to be unique, you can't have two identical 'handles' to different values. The values however can naturally be identical.
Individual values can be obtained through simple indexing.
value = my_map["cecar"]; // => -4
Indexing a value that doesn't exist will not generate an error, only the value 0. Be very careful of this since you might indeed have legal values of 0 in the mapping as well. i.e. a value of 0 might mean that the index has no value part but also that the value indeed is 0.
value = my_map["urk"]; // => 0
The preprocessor is not a part of the LPC language proper. It's a special process that is run before the actual compilation of the program occurs. Basically it can be seen as a very smart string substitutor; Specified strings in the code is replaced by other strings.
All preprocessor directives are given as strings starting with the character `#' on the first column of the line. You can put them anywhere, but as you'll be told later most of them do belong in the beginning of the code.
This is by far the most used preprocessor command. It simply tells the preprocessor to replace that line with the contents of an entire other file before going any further.
Data you put in include-files is usually data that won't ever change and that you want to put into several files. Instead of having to write those same lines over and over with the cumulative chance of putting in copying errors as you go, you simply collect that data into one or more files and include them into the program files as necessary.
The syntax is very easy:
#include <standard_file> #include "special_file"
NB! Note the absence of a ; after the line!
The two different ways you write this depend on where the file you want to include exists. There's a number of standard include files in the game, spread around in a number of different directories. Rather than having to remember exactly where they are, you can just give the name of the file you want to include then.
#include <stdproperties.h> #include <adverbs.h>
If you want to include files that aren't part of the standard setup, for example files of your own, you have to specify where they are. You do that either relative to the position of the file that uses it or by an absolute path.
#include "/d/Genesis/login/login.h" #include "my_defs.h" #include "/sys/adverbs.h" // Same as the shorter one above
When you include standard files, always use the <>-path notation. The reason isn't only that it becomes shorter and easier to distingues but also that if the files move around your program will stop working. If you use the <>-notation they will always be found anyway.
Include files can have any name, but as a rule they are given the '.h' suffix to clearly distinguish them as include files.
It is even possible to include c-files, i.e. to include entire
files full of code. However, doing that is very bad form. Do not
do that EVER! Why? Well, for one thing error handling usually
has a bad time tracing errors in included files, the line numbers gets wrong.
Also, since you include the uncompiled code into several different objects, you
will waste memory and CPU since these identical included parts has to be
compiled and stored separately for each object that uses them. Apart from all
this just reading the code will be a chore better not even contemplated.
What has the extension of the file name really to do with the
contents then? Well... actually nothing at all. However, the convention is to
keep code, functions that are to be executed, in c-files and
definitions in h-files. Usually the mudlib reflects on this
convention and might not recognize anything but c-files as code
sources.
This is a very powerful macro or substitute preprocessor command that can be abused endlessly. You are wise if you use it with caution and only for simple tasks.
The syntax is as follows:
#define <pattern> <substitute pattern> #undef <pattern>
Any text in the file that matches <pattern> will be
substituted for <substitute pattern> before compilation
occurs. A #define is valid from the line it is found on until the
end of the file or an #undef command that removes it.
Although defines can be written just as any kind of text, it is the custom (do this!) to use only capitals when writing them. This is so that they will be easily distinguishable for what they are since no one (not you either!) ever writes function or variable names with capitals.
Place all defines in the beginning of the file, or the poor chum who next tries to look at your code will have the devil's own time of locating them. If it's someone you asked for help (since your badly written code most likely won't work) he probably will tell you to stick the file someplace very unhygienic and come back later when you've learned to write properly.
Simple defines are for example paths, names and above all constants of any kind that you don't want to write over and over.
#define MAX_LOGIN 100 /* Max logged on players */ #define LOGIN_OB "/std/login" /* The login object */ #define GREET_TEXT "Welcome!" /* The login message */
Wherever the pattern strings above occur, they will be replaced by whatever is followed by the pattern until the end of the line. That includes the comments above, but they are removed anyway later.
tell_object(player, GREET_TEXT + "\n");
A comment on the // form is not a good thing since it
doesn't end until the end of the line.
#define GREET_TEXT "Welcome!" // The login message
...will be translated into the previous example as:
tell_object(player, "Welcome!" // The login message + "\n");
...which will have the effect of commenting away everything after the //,
all the way until the end of the line.
If a macro extends beyond the end of the line you can terminate the lines
with a \ which signifies that it continues on the next line.
However, you must break the string right after the \,
there must NOT be any spaces or other characters there, just
the linebreak.
#define LONG_DEFINE "beginning of string \
and end of the same"
Function-like defines are fairly common and often abused. The only really important rule is that any argument to the macro must be written so that they are used enclosed in parenthesis. If you don't do that you can end up with some very strange results.
1: #define MUL_IT(a, b) a * b /* Wrong */ 2: #define MUL_IT(a, b) (a * b) /* Not enough */ 3: #define MUL_IT(a, b) ((a) * (b)) /* Correct */
What's the big difference you may ask? Well, look at this example:
result = MUL_IT(2 + 3, 4 * 5) / 5; Expanded this becomes: 1: result = 2 + 3 * 4 * 5 / 5; // = 14, Wrong 2: result = (2 + 3 * 4 * 5) / 5 // = 12, Just as wrong 3: result = ((2 + 3) * (4 * 5)) / 5 // = 20, Correct!
Abuse of defines usually involves badly formulated macros, complicated macros used inside other macros (making the code almost impossible to understand) or humungous arrays or mappings in defines that are used often. The basic rule is to keep macros short and fairly simple. Do that and you'll never have any problems.
These are all preprocessor directives aimed at selecting certain parts of code and removing other depending on the state of a preprocessor variable.
The #if statement looks very much like a normal if statement,
just written a bit differently.
Assume you may have the following define somewhere:
#define CODE_VAR 2 or #define CODE_VAR 3
Then you can write
#if CODE_VAR == 2
<code that will be kept only if CODE_VAR == 2>
#else
<code that will be kept only if CODE_VAR != 2>
#endif
You don't have to have the #else statement there at all if you
don't want to.
It's sufficient to have the following statement to 'define' a preprocessor pattern as existing:
#define CODE_VAR /* This defines the existance of CODE_VAR */
Then you can write like this:
#ifdef CODE_VAR
<code that will be kept only if CODE_VAR is defined>
#else
<code that will be kept only if CODE_VAR isn't defined>
#endif
or
#ifndef CODE_VAR
<code that will be kept only if CODE_VAR isn't defined>
#else
<code that will be kept only if CODE_VAR is defined>
#endif
Again, the #else part is optional.
The #if/#ifdef/#ifndef preprocessor commands are almost only
used to add debug code that you don't want to have activated all of the time, or
code that will work differently depending on other very rarely changing
parameters. Since the conditions have to be hard-coded in the file and can't
change during the course of the use of the object this is something you very
rarely do.
This chapter will teach you what you need to know in order to actually produce code in the game environment. It will avoid the more complicated and unessential subjects, leaving them for chapter three. You will be taught a lot more about he mudlib and the workings of the gamedriver, knowledge necessary to produce working and effective code.
In order to provide you with examples of what I'm trying to teach you, I need to explain a few functions in advance. They will be repeated in their correct context later, but here's a preview so that you'll know what I'm doing.
To present things on the screen for the player to read, you use the efun write().
There's two special characters that's often used to format the text, `tab'
and `newline'. They are written as \t and \n
respectively. The `tab' character inserts 8 space characters and `newline'
breaks the line.
void write(string text)
e.g.
write("Hello there!\n");
write("\tThis is an indented string.\n");
write("This is a string\non several lines\n\tpartly\nindented.\n");
/* The result is:
Hello there!
This is an indented string.
This is a string
on several lines
partly
indented.
*/
If you have an array, mapping, or simply a variable of any kind that you want
displayed on the screen for debugging purposes the sfun dump_array()
is very handy. You simply give the variable you want displayed as an argument
and the contents (any kind) will be displayed for you.
void dump_array(mixed data)
e.g.
string *name = ({ "fatty", "fido", "relic" });
dump_array(name);
/* The result is
(Array)
[0] = (string) "fatty"
[1] = (string) "fido"
[2] = (string) "relic"
*/
Let's start by ripping off the rest of the LPC that was avoided in the first chapter. We need this in order to be able to actually create working objects in the game environment.
There are two kinds of function calls, internal and external. The only kind we have discussed so far is the internal one even though the external call has been displayed a few times.
Making an internal function call is as simple as writing the function name and putting any arguments within parentheses afterwards. The argument list is simply a list of expressions, or nothing. (A function call is naturally an expression as well).
<function>(<argument list>);
e.g.
pie = atan(1.0) * 4;
There is another way of doing this as well. If you have the function name
stored in a string and wish to call the function, you use the efun call_self():
call_self(<"function name">, <argument list>);
e.g.
pie = call_self("atan", 1.0) * 4;
If you use call_self() and specify a function that doesn't
exist, you will get an error message and the execution of the object will be
aborted.
An external call is a call from one object to another. In order to do that you need an object reference to the object you want to call. We haven't discussed exactly how you acquire an object reference yet, but assume for the moment that it already is done for you.
mixed <object reference/object path>-><function>(<argument list>);
mixed call_other(<ob ref/ob path>, "<function>", <arg list>);
e.g.
/*
* Assume that I want to call the function 'compute_pie' in the
* object "/d/Mydom/thewiz/math_ob", and that I also have the
* proper object pointer to it stored in the variable 'math_ob'
*/
pie = math_ob->compute_pie(1.0);
pie = "/d/Mydom/thewiz/math_ob"->compute_pie(1.0);
pie = call_other(math_ob, "compute_pie", 1.0);
pie = call_other("/d/Mydom/thewiz/math_ob", "compute_pie", 1.0);
As you can see, the efun call_other() works analogous to call_self().
If you make an external call using the object path, the so called master object will be called. If the object you call hasn't been loaded into memory yet, it will be. If an external call is made to a function that doesn't exist in the object you call, 0 will be returned without any error messages. Calls to objects that have bugs in the code will result in an error message and the execution of the object that made the call is aborted.
What's the big deal with call_self() then? Why can't you just
use call_other() all the time with the same result? Well, it has to
do with access to functions in an object, a part of LPC that I haven't gotten
around to explaining yet. However, the difference is that call_self
really works just like any internal call in regard to function modifiers and
function access, while call_other() is a pure external call.
Remember this for the future when differences between internal and external
access to functions are discussed.
You can call several objects at once just as easily as a single one. If you have an array of path strings or object pointers, or a mapping where the value part are path strings or object pointers you can call all the referenced objects in one statement. The result will be an array with the return values if you call using an array and a mapping with the same index values as the calling mapping if you give a mapping.
(array/mapping) <array/mapping>-><function>(<argument list>);
e.g.
/*
* I want a mapping where the indices are the names of the players
* in the game, and the values are their hitpoints.
*/
object *people, *names;
mapping hp_map;
// Get a list of all players.
people = users();
// Get their names.
names = people->query_real_name();
// Make a mapping to call with. Item = name:pointer
hp_map = mkmapping(names, people)
// Replace the pointers with hitpoint values.
hp_map = hp_map->query_hp();
// All this could also have been done simpler as:
hp_map = mkmapping(users()->query_real_name(), users()->query_hp());
Assume that you want to code an item like a door, for example. Doing that means that you have to create functionality that allows the opening and closing of a passage between two rooms. Perhaps you want to be able to lock and unlock the door, and perhaps you want the door to be transparent. All of this must be taken care of in your code. Furthermore, you have to copy the same code and make small variations in description and use every time you want to make a new door.
After a while you'll get rather tired of this, particularly as you'll find that other wizards has created doors of their own that work almost - but not quite - the same way your does, rendering nifty objects and features useless anywhere but in your domain.
The object oriented way of thinking is that instead of doing things over and over you create a basic door object that can do all the things you want any door to be able to do. Then you just inherit this generic door into a specialized door object where you configure exactly what it should be able to do from the list of available options in the parent door.
It is even possible to inherit several different objects where you can combine the functionality of several objects into one. However, be aware that if the objects you inherit define functions with the same names, they will indeed clash. Just be aware of what you are doing and why, and you won't have any problems.
The syntax for inheriting objects is very simple. In the top of the file you write this:
inherit "<file path>"; e.g. inherit "/std/door"; inherit "/std/room.c";
NB! This is NOT a preprocessor command, it
is a statement, so it does NOT have a # in front
of it, and it is ended with a ;. As you see you may specify that
it's a c-file if you wish, but that's not necessary.
The child will inherit all functions and all variables that
are declared in such a way as to permit inheriting. If you have a function with
the same name as a function in the parent, your function will mask
the parent one. When the function is called by an external call, your function
will be executed. Internal calls in the parent will still go to the parent
function. Often you need to call the parent function anyway from the child, you
do that by adding :: to the internal function call.
void
my_func()
{
/*
* This function exists in the parent, and I need to
* call it from here.
*/
::my_func(); // Call my_func() in the parent.
}
It is not possible to call a masked function in the parent by an external call, it is only available from within the object itself. If an object inherits an object that has inherited another object, e.g. C inherits B that inherits A, then masked functions in A is only available from B, not from C.
There's a functionality called shadowing available in LPC. Purists tend to use the word 'abomination' and scream for its obliteration, since it goes against most of what's taught about proper control flow and object purity. For gaming purposes it's rather useful, although it can cause a host of problems (particularly when it comes to security). Use it with caution!
It's possible to make an object shadow another object. What happens then is that the functions and global variables in the shadow object that also exist in the shadowed object mask the original. Calls to the shadowed functions will go to the shadow instead. The shadow will for all practical appearances 'become' the object it shadows. As you can see this is done in runtime, and not during compilation.
This is all I will say in this respect right now. How to create shadows and use them will be handled in detail later. For now this is what you need to know.
[intp, floatp, functionp, stringp, objectp, mappingp, pointerp]
Due to the fact that all variables are initialized to 0, and that many
functions return 0 when failing, it's desirable to be able to determine what
kind of value type you actually have received. Also, if you use the mixed
type it's virtually essential to be able to test what the variable contains at
times. For this purpose there's a special test function for each type that will
return 1 (true) if the tested value is of the asked for type, and 0 if not.
NB! These functions test the type of the value, NOT
the value itself in the sense of truth functionality. In other words intp(0)
will always evaluate as true, as will mappingp(([])).
The very types you assign variables and function can have qualifiers changing the way they work. It's very important to keep them in mind and use the proper qualifier at the proper time. Most work differently when applied to variables rather than functions, so a bit of confusion about how they work usually is quite common among the programmers. Try to get this straight now and you'll have no problems later
This is a problematic qualifier in the respect that it works differently even for variables depending on where they are! Global variables, to begin with, are (as you know) variables that are defined in the top of the file outside any function. These variables are available in all functions i.e. their scope is object-wide, not just limited to one function.
It is possible to save all global variables in an object with a special efun
(described later). However, if the global variable is declared as static,
it is not saved along with the rest.
static string TempName; // A non-saved global var.
A function that is declared static can not be called using
external calls, only internal. This makes the function 'invisible' and
inaccessable for other objects.
A variable or function that has been declared as private will
not be inherited down to another object. They can only be accessed within the
object that defines it.
Functions and variables that are declared as nomask can not be
masked in any way, neither by shadowing nor inheriting. If you try you will be
given an error message.
This is the default qualifier for all functions. It means there is no limits other than those which the language imposes on accessing, saving and masking.
Sometimes you want a function to be able to receive a variable amount of arguments. There's two ways of doing this and it can be discussed if it's correct to put both explanations in this chapter, but it's sort of logical to do so and not too hard to find.
A function that is defined varargs will be able to receive a
variable amount of arguments. The variables that aren't specified at the call
will be set to 0.
varargs void
myfun(int a, string str, float c);
{
}
The call myfun(1); will set a to 1, str
and c to 0. Make sure you test the potentially unused
variables before you try to use them so that they do contain a value you can
use.
There's another way as well. You can specify default values to the variables
that you're uncertain about. Then you don't have to declare the function varargs
and you will have proper default values in the unused argument variables as
well.
void
myfun(int a, string str = "pelle", float c = 3.0);
{
}
This function must be called with at least one argument, the first, as it
wasn't given a default value. The call myfun(1, "apa");
will set a to 1, str to "apa"
and c to 3.0.
There's one data type that I more or less ignored earlier, and that's the function
type. Just as there's a type for objects, functions have a type as well. You can
have function variables and call assigned functions through those variables.
Mostly the function type is used in conjunction with other functions that use
them as parameters.
You declare a function type just as any variable:
<data type> <variable name>, <another variable>, ..., <last variable>;
e.g.
function my_func, *func_array;
Assigning actual function references to them, however, is a bit tricker. You can assign any kind of function to a function variable; efun, sfun or lfun is just the same. You can even assign external function references.
Assigning a function reference requires that the function already is defined, either itself or by a function prototype in the header. Let's assume for now that you're only interested in the simple reference to the function.
<function variable> = <function name>;
<function variable> = &<function name>();
e.g.
my_func = allocate;
my_func = &allocate();
Usage of the new function reference is done just as with the ordinary function call.
int *i_arr; i_arr = allocate(5); // Is the same as... i_arr = my_func(5); // ... using the function assignment above.
This will be enough for now. Later I'll explain how to create partial function applications, internal and external function declarations and how to use them in complex function combinations.
The LPC switch statement is very intelligent, it can also use ranges in integers:
public void
wheel_of_fortune()
{
int i;
i = random(10); // Get a random number 0 - 9
// Strictly speaking, this local variable isn't
// necessary, it's just there to demonstrate the
// use and make things clearer. I could have
// switched on 'random(10)' directly instead if
// I had wanted to.
switch (i)
{
case 0..4:
write("Try again, sucker!\n");
break;
case 5..6:
write("Congrats, third prize!\n");
break;
case 7..8:
write("Yes! Second prize!\n");
break;
case 9:
write("WOOOOPS! You did it!\n");
break;
default:
write("Someone has tinkered with the wheel... Call 911!\n");
break;
}
}
It happens now and then that you need to make function calls you know might
result in a runtime error. For example you might try to clone an object
(described later) or read a file. If the files aren't there or your privileges
are wrong you will get a runtime error and execution will stop. In these
circumstances it is desireable to be able to intercept the error and either
display some kind of message or perform other actions instead. The special LPC
function operator catch() will do this for you. It returns 1 (true)
if an error occurs during evaluation of the given function and 0 (false)
otherwise.
int catch(function)
e.g.
if (catch(tail("/d/Relic/fatty/hidden_donut_map")))
{
write("Sorry, not possible to read that file.\n");
return;
}
It's also possible to cause error interrupts. This is
particularly useful when you want to notify the user of an unplanned for event
that occured during execution. Typically you want to do this in the 'default'
case of a switch statement, unless (naturally) you use default as a sort of
catch-it-all position. In any case throw() will generate a runtime
error with the message you specify. A catch() statement issued
prior to calling the function that uses throw() will naturally
intercept the error as usual.
throw(mixed info)
e.g.
if (test < 5)
throw("The variable 'test' is less than 5\n");
In comp sci terms, arrays and mappings are used as reference by pointer and the other types as reference by value. This means that arrays and mappings, unlike other variables, aren't copied every time they are moved around. Instead, what is moved is a reference to the original array or mapping. What does this mean then?
Well... simply this:
object *arr, *copy_arr;
arr = ({ 1, 2, 3, 4 }); // An array
copy_arr = arr; // Assume (wrongly) that a copy_arr becomes
// a copy of arr.
// Change the first value (1) into 5.
copy_arr[0] = 5;
... Now... this far down the code it's logical to assume that the first value
of copy_arr is 5 while the first value or arr is 1.
That's not so however, because what got copied into copy_arr was
not the array itself, but a reference to the same array as arr.
This means that your operation later where you changed an element, changed that
element in the original array which both variables refer to. copy_arr
and arr will both seem to have changed, while in fact it was only
the original array that both refered to that changed.
Exactly the same thing will happen if you use mappings since they work the same way in this respect.
So... how do you get around this then? I mean... most times you really want to work on a copy and not the original array or mapping. The solution is very simple actually. You just make sure that the copy is created from another array or mapping instead.
_ This is just an empty array
/
copy_arr = ({ }) + arr;
\_ This is the one we want to make unique
In this example copy_arr becomes the sum of the empty array and
the arr array created as an entirely new array. This leaves the
original unchanged, just as we wanted. You can do exactly the same thing with
mappings. It doesn't matter if you add the empty array or mapping first or last,
just as long as you do it.
There's a lot of stuff you want to do, like handling strings and saving data to files, that's not exactly LPC. It's part of the 'standard function package' that most programming languages sport. This chapter will teach you the basics of how to do all the things you need in order to create LPC objects.
Objects in the game share a certain set of common properties, the ones you always can rely on to be there for any kind of object are these:
root for admin objects and backbone
for the rest.
As I have explained earlier the gamedriver really knows very little about the actual game, actually as little as possible. Instead the mudlib is entrusted to take care of all that. So, what we have done is to try to work out what basic functionality is needed, things like how objects should interact with players, moving, light-level descriptios etc. Then we have created basic object classes that implement these functionalities in actual code.
A domain wizard doesn't have spend endless hours trying to figure out how to make an object work in relation to others in respect to basic functionality. Instead he just makes his object inherit the standard object suitable for the task he wants to code. Then he just adds the bits and pieces of code to the object that is necessary to make it unique and do the things that are special for that particular object.
A consequence of this naturally is that all objects in the game rely 100% on the fact that a certain type of object (room, monster, gadget) has a certain set of common functionality. They simply have to have that in order to be able to interact in the agreed way, if they didn't, if people had different ways of solving the same problem, the objects would only work with a certain wizard's area and never outside of it. It would then not be possible use a sword all over the game, it wouldn't even be possible to move it around from place to place. Naturally this means that we enforce this unity, and therefore it is impossible to create (and use) objects that don't inherit these special objects. Sure, as you can see for yourself later, it is possible to create a sword that doesn't make use of the standard weapon object, but it is perfectly impossible to wield it...
The standard objects provide certain basic functionality that must exist in all objects and they also make a bit of sanity checking on values of certain variables, but the latter really is a very minor functionality.
There are standard objects for a lot of purposes, the most important one is `/std/object.c' though.
This is the all purpose object class. ALL objects in the game must inherit this object somewhere along the line if they are to be 'physically' present somewhere. Using any kind of standard object usually insures that this one is inherited as well, since they already make use of it.
The standard object defines the following conventions:
There exists a number of standard object classes for use in various situations. You will need to read the separate documentation on each and every one of them in order to fully learn to use them. However, this summary of the available classes will at least point you in the correct direction as you go.
As stated earlier you have to inherit most of these objects in order for your derived objects to work at all. However, it is possible to create new versions of some of them on your own. Again, I must emphasize that this is not a good idea since you then deviate from the common base of object functionality that is in use in the game. Problems that occur because of this actually are tricky to catch and track down since it isn't the first thing you suspect that someone has done.
These objects are more of the order of 'help' clases. They don't qualify as objects in their own right, but they provide neat functionality for what they do.
Objects, as previously described, comes in two kinds - master objects and clones. In general you tend to use cloned objects. At least for objects that are being 'handled' in the game, objects that you can move about, touch, examine etc, or any object that exist in more than one copy. Making exclusive use of only the master object is usually only done for rooms, souls or dameon objects of various kinds.
Naturally any object in the game must have a master object. An object is loaded into memory and the master object created when a function (any function call, even to a non-existing function) is called in it. Cloning it just makes identical copies of it. If you destroy the master object, the gamedriver will have to load it again later before making any new clones. Naturally this is what you do every time you have made changes to the object that you want to become active. Destroying the master object won't change the already existing clones, of course. You'll have to replace them separately.
The mudlib in fact works so that loading an object is made by calling a non-existing function in the object and updating it simply destroys the master object.
How to get the object references then? Well, that depends on the situation. An object reference is either an object pointer or a string path, referring to the object source in the mud filesystem. Obtaining them is different depending on the situation however. Let's go through them all.
[this_object, previous_object, calling_object]
An object can always get the object reference to itself.
Use the efun this_object():
object this_object()
e.g.
object ob;
ob = this_object();
In order to find out which object called the currently running function in an object using an external call, you can use the efun 'previous_object()':
object previous_object(void|int step)
e.g.
object p_ob, pp_ob;
p_ob = previous_object(); // The object calling this function.
pp_ob = previous_object(-2); // The object calling the object
// calling this function.
If you supply no argument or -1, the function will return the immediately
previous object that called. Decrementing the argument further will return even
more previous callers, i.e previous_object(-4) returns the object
that called the object that called the object that called your object. If indeed
the chain of calling objects was that long. When you exceed the length of the
calling chain beyond the first object that made a call, the function will return
0.
As I hope you noticed, this call only checks for external calls, not internal. There is a corresponding efun that works just the same but for any type of call (internal or external) that has been made:
object calling_object(void|int step)
The usage is the same however.
So... how do you know if the object reference you just received is a valid
object or not (i.e. 0 or something else)? Well, use the nice efun objectp()
as described earlier. It returns 1 if the argument is a valid object pointer and
0 otherwise.
int objectp(mixed ob)
e.g.
if (objectp(calling_object(-2)))
write("Yes, an ob calling an ob calling this object exists!\n");
else
write("No such luck.\n");
[setuid, getuid, seteuid, geteuid, creator, set_auth, query_auth, clone_object]
First of all you must make sure that the object that tries to create a new object has the privileges required to do so. The rules are pretty simple actually: An object with a valid euid can clone any other object. A valid euid is anything except 0. The euid 0 is the default uid and euid on creation of an object, and it's used as meaning 'no privileges at all'.
However, usually the choice of euids you can set is pretty limited. If you're a wiz it's usually limited to your own name. A Lord can set the euid in an object to be his, or any of the wizard's in the domain (unless one of the wizards is an Archwiz, then that one is excempt as well). And naturally objects with 'root' uid can set any euid they like.
So... the uid of the object determines what choice of euids you have. You set the uid to the default value by adding this sfun call:
void setuid()
e.g.
setuid();
Simple eh? Doing that sets the uid to the value
determined by the location of the object source-file in the mud filesystem. The
rules for this is the same as for the creator value described
earlier. You can get the creator value of an object with the sfun creator(),
it simply returns the string setuid() would use for that object.
string creator(mixed reference)
e.g.
string my_creator;
my_creator = creator(this_object());
To get the actual uid value that is currently used, you
the sfun getuid()
string getuid()
e.g.
string curr_uid;
curr_uid = getuid();
So.. the uid is now set to the highest privilege giver. The euid however, is still 0. Since the euid determines the actual privileges used in an object this means that the object still has no privileges at all.
To set the euid you use the sfun seteuid(),
the argument given will be set as euid if allowed (it's tested). The function
returns 0 on failure and 1 on success. If you don't send any argument, the euid
is set to 0, 'turning it off' so to speak.
int seteuid(void|string priv_giver)
e.g.
if (seteuid("mrpr"))
write("Yes! I'm the ruler of the UNIVERSE!\n");
else
write("Awwwww....\n");
Naturally there's a corresponding sfun to return the current euid:
string geteuid()
e.g.
write("The current euid = " + geteuid() + "\n");
The sfuns setuid(), getuid(),
seteuid() and geteuid() are all using the efuns set_auth()
and get_auth(). They are used to manipulate a special authority
variable inside the object in the gamedriver. The gamedriver will call a
validizing function in the master object (security) if you try to use set_auth()
to make sure that you are privileged to do so. The reason is that it's possible
to store any kind of string in the authority variable, and the way we use it is
merely a convention, something that we have decided is the best way of solving
security.
When you try to perform a privileged operation, like writing to a file or
cloning an object the gamedriver calls other special functions in the master
object to make sure you have the right privileges. They all depend on that the
information stored in the authority variable is formatted in the special way we
want for it to work properly. Due to this fact you are not allowed to use set_auth()
in any other way than already is allowed by setuid() and seteuid(),
so there's really no use in doing that at all. query_auth() is not
protected but you won't find much use for that information anyway.
The information stored in the authority variabl