Discworld Documentation:

LPC for Dummies

Written by Drakkos Wyrmstalker

N.B - This is a work in progress... a living document if you like. If it appears to be dead when you view it, don't worry. It's most likely just playing possum.

Comments on these chapters and tutorials can be e-mailed to Drakkos.

Chapter One: Overview of LPC

LPC is an interpreted MUD language created by Lars Pensjoe for his LPMUD, an interactive multi-user environment. LPC is suitable for many purposes, but is especially suited to the creation of games such as Multi User Dungeons. Since its first appearance in 1988, the sophistication and popularity of the language has increased dramatically.

LPC is like the programming language C in certain respects. It isn't quite as free-form as C, but it does offer a development environment extremely well suited to creating multi-user dungeons. Here is where you, as a creator, come in.

When writing a program for a computer, it is necessary to convert the language being used into the primitive grunts and whistles that the machine itself understands. Computers, at the lowest levels, understand only binary (1s and 0s). Obviously, coding a complicated program in binary would be extremely time-consuming and thoroughly mind-bending... and so, programming languages were created to allow for computers to be programmed in a language that was easier to learn.

High and Low Level Languages

Low level languages, such as Assembler, aren't all that incredibly different from the signals the computer itself understands. Coding in a low level language takes time and a good understanding of how the internals of a computer work.

An assembler program may look something like the following example. Don' t worry about understanding it... you won't need to comprehend how this works... it is included merely as an example:

mov ax,0006
mov bx,0002
add ax,bx

What this program does is move the value 0006 into the register the computer understands as 'ax', then moves the value 0002 into the register the computer understands as 'bx'. It then adds the contents of registers 'ax' and 'bx' together.

As you can see, coding in assembler requires quite a bit of work to just add two numbers together.

High Level languages, on the other hand, are written using languages somewhat more similar to the language we speak. Mainly they use english syntax to build complex instructions for the computer. A very well known example of a high level language is BASIC (Beginners All-Purpose Symbolic Instruction Code).

An example of adding two numbers together in a high level language would simply be:

value = 6 + 2

As you can see, this is much easier to follow, and does not require any knowledge of how the computer is accomplishing the task at a machine level.

Languages such as C and Pascal fall a little between these two extremes, and so are often referred to as Mid Level languages. In order for the computer to understand a high level language, it is necessary for the code to be converted into machine language. This occurs in two main ways.

Compilation, and Interpretation.

In compiled languages, you type up some source code in the high level syntax, then 'compile' it. The compiler takes your code, and converts it into machine language, producing an executable file at the end. You then run this file, and if it works, great! If not, you need to re-edit the source code, and then recompile.

Interpreted languages, on the other hand, work by converting the code into machine language as it is executed. This is obviously much slower than with a compiled language, but does allow for changes to be made without requiring the whole file to be recompiled. Statements are executed one after another, and then forgotten about.

Well... this isn't entirely true in the case of LPC. LPC is an 'object orientated' language... each piece of code you write is potentially an object. Put simply, an object is a program that has been loaded into memory. The first time an object is loaded, it becomes the 'master object'. When this master object is created, it is translated from high level LPC into an intermediate language suitable for speedy interpretation when the game is running. All 'clones' of this object are then made from this master object.

Take the following example:

There is a file, 'Monster', which contains all the code necessary for creating your very own critter. When the 'monster' object is loaded, it becomes the 'master' object for monster, and is semi-compiled by the driver (more on the driver later). When you create your own monster, it makes a copy of the master object, and gives your new monster a chunk of the computer's memory to deal with the new object. We will explain why this is necessary later on when we come to talk about data types.

The Driver and the Mudlib

Before we do that, we need to cover what exactly we mean by the terms 'mudlib', and 'driver'. You'll come across these terms rather often as a creator, so it is necessary to make sure you fully understand them now.

Essentially, the driver is a piece of very low level code that deals with all the things necessary to keep the mud running. For example, the driver deals with the network connections when people log on to the MUD, and makes sure the proper login connections are made. It also deals with creating new objects, and destroying objects when they are no longer being used. It is the driver that interprets your LPC code to make all sorts of groovy things happen. The driver also provides a library of functions called 'efuns' which are very useful when coding. We will discuss these in more detail when we start to talk about LPC functions. All you really need to understand about the driver is that it's somewhat like a mini-operating system for the MUD.

The mudlib, on the other hand, is simply the collection of LPC objects that make up the basic environment of the MUD. On Discworld, this is essentially all code outside of /w/ and /d/. For example, the objects that make up rooms, monsters, and weapons all belong to the mudlib.

The relationship between the mudlib and the driver can be summed up as: The mudlib knows what to do, but has no idea how to do it. The driver knows how to do it, but has no idea what to do. The mudlib and driver are co-dependant, and from the combination of the two comes the basic structure of the MUD itself.

The Preprocessor.

The preprocessor is not actually a part of LPC. What it is is a clever string substituter that is run before an object actually compiles. If you have a look at some of the code we have on Discworld, you'll see many of them are peppered with lines that begin with the symbol '#'. These are not actually LPC statements. Instead, they are 'preprocessor directives'... secretive instructions that are handled before an object ever compiles.

You won't need to think too much about how this works. Just so long as you know that it happens. You will need, however, to understand how two of the most common directives are used.

The first of these is #include. This does exactly what it says it does... it simply dumps a copy of a file in place of the #include line. Generally these files are called 'header files', and are denoted with a .h extension to the filename. The filename will need to be enclosed in either quotation marks (""), or angle braces (<>). The difference here is purely in which order the preprocessor will look for the file. If you enclose it in quotation marks, it will look in the current directory for the file. If it doesn't find it, it then looks in the standard include directory (On Discworld, this is /include/). If enclosed in angle braces, it will search in the opposite order... first in /include/, and if it doesn't find it there, then in the current working directory.

For example:

#include "my_header.h"

Will search the current directory for a file called 'my_header.h'. If it doesn't find it there, it will then search /include/ for a file called 'my_header.h'. If it finds the file, it will then replace the #include line with the contents of it.

The next preprocessor directive you are likely to come across is the #define directive, and this takes the form:

#define NAME "drakkos"

When the preprocessor comes to this line, it will search through the code for any occurrence of the text NAME, and replace it with the text "drakkos". This is extremely useful for defining things such as filenames, since it means a filename which is used throughout your code can be changed simply by altering the #define.

#include files are often used mainly as a collection of related #defines... think of it as a collection of shortcuts that can be used instead of the real values or file-names.

Now, let's take a look at the preprocessor in action on a small piece of code. Let's say we have a header file, my_header.h, that consists of the following code:

#define NAME "drakkos"
#define MUD "Discworld"
#define VALUE "10"

And say we have a piece of code such as the following:

#include "my_header.h"

write("My name is " + NAME);
write("I am a creator on " + MUD);
write("I am " + VALUE + " years old.");

Now, let's look in stages at what the preprocessor does. First, it comes to the line #include "my_header.h". So it looks in our current directory for the file, and lo and behold, there it is. So it dumps this file in place of the #include statement:

#define NAME "drakkos"
#define MUD "Discworld"
#define VALUE "10"

write("My name is " + NAME);
write("I am a creator on " + MUD);
write("I am " + VALUE + " years old.");

It then comes to the first #define statement. It searches through the code for occurrences of NAME, and replaces it with "drakkos":

#define NAME "drakkos"
#define MUD "Discworld"
#define VALUE "10"

write("My name is " + "drakkos");
write("I am a creator on " + MUD);
write("I am " + VALUE + " years old.");

Then the next define, where it replaces MUD with "Discworld":

#define NAME "drakkos"
#define MUD "Discworld"
#define VALUE "10"

write("My name is " + "drakkos");
write("I am a creator on " + "Discworld");
write("I am " + VALUE + " years old.");

And then VALUE with "10":

#define NAME "drakkos"
#define MUD "Discworld"
#define VALUE "10"

write("My name is " + "drakkos");
write("I am a creator on " + "Discworld");
write("I am " + "10" + " years old.");

There are other directives that have not been discussed here... these are much rarer and generally you will not have cause to use them in day to day creating. As long as you understand how #define and #include work, you're well on your way to appreciating what the preprocessor does.

Chapter Summary

In this chapter, you should hopefully be comfortable with the difference between the mudlib and the driver, and compiled/interpreted languages. You should also be comfortable with the use of #include and #define in objects, and how these can be used to develop portable and easily modified code.