MERC Programming FAQThis document contains a description and discussion of several frequently attempted features implemented in MERC derived MUDs. This is in no way an introduction to C programming - the reader is assumed to be familiar with basic programming techniques. Currently, the document contains only descriptions of how the features were implemented in my MUD, Abandoned Reality. I welcome any new additions, but send an email first, outlining what you want to add, before writing it in full. In additions, there's a short section about what tools can be used to make developing a MERC MUD easier. I'd like to recommend that you also visit these pages which contain similar information, which is kept better up to date:
Thanks to Patrick 'Sotty' Scheele and Catherine 'Zoia' Allen for proofreading. Last updated: July 99 (minor fixes and links added) |
|
DESCRIPTOR_DATA - there's one for each connection to the MUD. It contains the file descriptor number of the socket - the two-way channel passing data between the MUD and the player's computer as well as other information which is specific to a connection: host name, connection state (described in details later %%), incoming commands not yet executed and outgoing data not yet written. All descriptors are stored in descriptor_list.
CHAR_DATA - describes a character: a mob or a player. Contains data like current/max hp/mana/move, name, description, current room, objects carried etc. All characters are stored in char_list.
PC_DATA - if the character is a player, the CHAR_DATA will contain a pointer to this structure. It contains data which only players have, like thirst, hunger, prompt, title.
OBJ_DATA - contains data bout the instance of an object, like weight, type of item, location etc. Item specific data is handled by an array of 4 integers, value. Each field has a different function depending on type of item. All objects are stored in object_list.
MOB_INDEX_DATA - contains mob prototypes, read in from the area files. Created mobiles inherit their data from this structure. All mobile prototypes are stored in mob_index_hash.
OBJ_INDEX_DATA - as MOB_INDEX_DATA, but for objects. All object prototypes are stored in obj_index_hash.
ROOM_INDEX_DATA - contains the room data. Note that rooms do not have prototypes like objects and mobs. All rooms are stored in room_index_hash.
main will, after parsing some switches, setting up the time and binding to the listening socket at the specified port, call load_db which opens AREA.LST and reads in each area, calling load_mobiles, load_rooms etc. in turn.
After all areas are loaded, control is passed to game_loop_unix. This function will check if any new input has arrived at any of the connected sockets, using the select system call. There is a main control socket, which was acquired when binding to the port before - if that one receives input, it means a new connection can be established. If this happens, new_descriptor is called, which is responsible for setting up a new descriptor and adding it to the descriptor_list.
char_list links together all characters (CHAR_DATA) and mobs active in the game. Each node in the list has a next pointer pointing to the next one. Note that players' characters that are logging in are not actually put in this list until they have entered the game. Mobs are added to this function by create_mobile, players are added by the nanny function as they enter the game after typically reading MOTD. Extraction from the list happens with extract_char.
object_list works as above, but for all instances of objects (OBJ_DATA) in the game. Unlike for characters, the equipment of players that are logging in does get put into this list. This can lead to some surprises if you are not careful. Objects are removed using extract_obj, which removes the current objects and recursively calls itself for all its content.
descriptor_list contains all the DESCRIPTOR_DATA structures. New entries in this list are added in the new_descriptor functions and removed using close_socket.
Each ROOM_INDEX_DATA holds a list of people in the room, surprisingly enough called people. This linked list is threaded by the next_in_room field, not next. A common mistake which I have made often enough is to start at e.g. ch->in_room->people to go through all characters in room, but use next rather than next_in_room. The room has also a contents field, which has a list of objects in the room - these are threaded via the next_content variable.
Characters are moved from rooms using char_from_room(ch) and char_to_room(ch, new_room). Those functions modify the linked lists of the rooms involved and adjust a few other things, like light level. Note that char_from_room does not require a room to remove the character from, it already knows it. The same holds true for all the other X_from_Y functions which remove an object/character from an object/character/room.
Objects are more complicated. To move an object into a room, you should first remove it from its current place using obj_from_obj(obj) if it is inside another object, obj_from_char(obj) if carried by a character or obj_from_room(obj) if in a room. Then, use obj_to_room(obj,room) to add the object to the linked list of the room.
Characters themselves have a list of objects they carry - ch->carrying. This lists contains worn objects too (objects in inventory have their wear_loc field set to WEAR_NONE while as worn objects have it set to e.g. WEAR_HEAD. The function obj_from_char will remove an object from a player, adjusting the carried weight and number of carried items on this character. The function obj_to_char(obj,ch) will do the opposite.
Finally, objects have a list of of objects they contain, obj->contains. Objects can be put into another object using obj_to_obj(obj, obj_to_put_it_into) and removed using obj_from_obj(obj). Both will adjust the carried weight of the character holding the objects involved, if any.
How do you know where an object or character is? Each one has several pointers pointing at their current location. For characters, ch->in_room points at the current room. Characters that are not dead must always be in a room, since a lot of update functions assume this.
For objects, obj->in_room will point at the room the object is directly in. obj->carried_by points at the character that is carrying this object and obj->in_obj points at the container this object is in. Note that only one of these is set to a non-NULL value at any time: if a pen is in a shirt, and the shirt is lying on the floor of a room, the pen's in_obj will be set to the shirt, but the pen's in_room will be set to NULL. The next_content field is always used to mark the next objects after this in be it in a room, on a character or inside another object.
For more information about lists, take a look at Peter Vidler's linked list information.
for (obj = ch->carrying ; obj ; obj = obj->next_content) { /* Do something with obj */ } |
The above code is safe as long as you do not do anything about the objects. If whatever you desire to do with the characters or objects may destroy them or move them to another room, you will have to use a temporary variable to store the next object to be checked, BEFORE doing anything to it:
for (obj = ch->carrying; obj; obj = obj_next) { obj_next = obj->next; /* Do something catastrophic with obj */ } |
This is necessary because if you extract obj, the memory may no longer be accessible and should not be referred to (see Memory Allocation for more details). If you move the object to say, another room, its obj->next_content would point at the second object in that room, resulting in the code you probably intended to be executed only on ch's inventory to be executed in objects in that other room.
Since you are already familar with C, you have typically used malloc and free to allocated and free dynamic memory. Since most of the data in a MERC derived MUD is dynamic and stored in lists, dynamic memory allocation is a very important part of the MUD. MERC does not use the standard library memory management routines - since we know how data is allocated, when it needs to be freed etc. it is more efficient to take care of memory management ourselves.
The basic memory allocation function is alloc_perm. This function allocates some permanent memory - the memory can never be freed. alloc_perm starts by grabbing a 128 kilobyte chunk of memory, remembering how far in it it has come and otherwise returning the pointer to the free spot. If there is not enough room, anther 128 kilobyte chunk is allocated.
alloc_mem is more like malloc: it first rounds up the requested memory size to fit in the rgMemSizeTable (%%)?. It keeps a list of free chunks of the given memory size in an array: if no more chunks of that size are free, alloc_perm is called. No further information about the chunk size is saved: thus it is necessary to call free_mem with the size of the chunk when freeing it, so it can be put back on the right free list.
alloc_mem is useful in the cases where you need memory of variable size - like for example the output buffer.
Most structures in the game are allocated using alloc_perm. If the structure is then freed, it is put on a freelist (e.g. char_free) rather than freeing the memory. This results in lower memory overhead - when allocating an e.g. 140 bytes char_data structure with alloc_perm, only 140 bytes are allocated. If alloc_mem was used, a request for 140 bytes would allocate a 256 byte chunk. Recycling is also fast: when the freed memory is required again, all it takes is checking the relevant freelist and then removing the first element if one is available.
Finally, there is the hashed string memory region. When booting, a MAX_STRING_SIZE chunk of memory is allocated. Whenever a string is read from the area files using fread_string, it is put in that area (without any memory overhead: the string is assumed to be permanent). The string is also searched for before inserting it so duplicate strings only take up the memory of one string. To speed up the search, strings are hashed by their length (so to see if one 12-byte long string has already been read, you would only have to compare to other 12-byte long strings).
After the bootup, new strings are no longer shared. Calling str_dup, which duplicates a string, on a shared string returns simply that shared string. Calling free_string on it has no effect. Calling str_dup on any other memory will allocate enough space using alloc_mem then copy the string over.
Shared String Manager is a C-port of a C++ String class that Fusion wrote for MUD++. SSM keeps a reference count on all strings, and allows sharing of strings. This is especially useful when using some sort of OLC - then, changing of a string like a room description will free the memory.
Initial versions of SSM had a few shortcomings: most notably, a very slow hashing algorithm. Oliver Jowett has since fixed this-and-that and I have provided a new hashing algorithm for SSM and the results are available from %%.
There are some modifications required in order to use SSM. There is some code, most notable in create_object and create_mobile that assumes that the permanent strings can never be freed, and when duplicating them, it simply assigns the string (e.g. mob->name = pMobIndex->name). This gives a problem when the string is freed using free_string when the mob is deleted, since SSM will count down the reference count, find out that the string is no longer in use (assuming that only one mobile was created) and recycle it next time some memory for a string is requested. It is therefore important to use str_dup to duplicate and free_string to free strings.
This chapter will describe an implementation of an event queue system. There might be some confusion whether a MUD is event based: a true event-based system is one where events are used for communication between the objects (%% what MUD server does implement this?) - for example, to move a character out of a room, an event is posted saying that this character wants to move this way. The event would get to e.g. the door in that direction, that would then find out that the character cannot pass through it, send an appropriate message to the character and then respond negatively.
This makes modularized (%%?) programming a lot easier: rather than having to add yet-another-special-case in move_char for say, a spell that blocks an exit for a while, you would instead simply create an object (not really a physical object, just an entity capable of receving events), place it in a room and let it respond to the events of type EV_WANTS_TO_MOVE_THIS_WAY. Without changing move_char.
I don't really know of any MUD server that uses this method - the above description is an approximation based on articles posted to rec.games.mud.admin about this topic. I am personally familiar with this event-based programming methods from User Interface Libraries like Turbo Vision and Windows: there events (also called, perhaps more properly messages) are passed around. For example, before closing a window, an event inquirying whether the window can be closed would be sent to objects of the window. Then, the input box that must contain for example, values within a certain range would catch this event, and validate that the data is really within the range, responding negatively if this was not the case.
While this event-based programming is something with clearly a large number of possibilities, it is not what I will cover here - I'm not even sure implement such a scheme in a MERC based MUD would be feasible (although at some point I'll certainly will try doing it). Properly the following scheme should be called event-queue-based but it is often, perhaps incorrectly, referred to as event-based.
%%Rearrange these paragraphs
Let us take a look at typical implementation of mobs that move around on a polling-based (As opposed to event-queue-based) MUD. Each 5 seconds, all the characters on the MUD are checked: if the character is an NPC, is not fighting and is not a sentinel mob, it has a 25% chance of moving around.
When it finally moves, all the mobs move at once: this may seem somewhat strange, and it might, in case of things that are performed regularily and take up a signifcant amount of time, create lag: if we, hypothetically, assume that moving a mob will take 0.01 seconds and every 5 seconds we move 100 of them: then every 5 seconds there will be a noticable 1 second of lag. If we could instead move 20 mobs each second, the lag would be only 0.2 seconds. But, how to do that without having to run through the list 5 times as often, creating even more lag?
How could we check only on those mobs that were capable of moving, and never have to worry about players and sentinels? This technique is referred to as polling: checking all the time if the object is ready for something.
The easy solution to recuce overhead is to create another list, containing the mobs capable of moving in this way. When creating a mob, if not sentinel, put it on the list.
However, you will then find that you could also do the same for fighting, for decaying objects, for mobs that pick up stuff etc. Using an event queue, you can merge all these things.
The event queue is simply a list of events, things that are to happen at some time in the future. Each event stores the time at which it will go off, the primary actor, the type of the event and typically a pointer to a callback function that will be called when the event goes off.
In our above case, we would when the mob gets created, create a new event with tne mob as the actor, and the callback function pointer set to some function like "move_mob". Since the mobs normally have a 25% chance of moving every 5 seconds, we could approximate this by setting the time when the event will go off to somewhere between 5 and 20 seconds.
Each game pulse (which is usually 1/4 of a second), the events that should go off at that pulse are one by one removed from the queue and executed. The mobs move, seldom together, and they move without having to run through the list of all the mobs and ask them if they want to. At the end of the move_mob event, the code would typically reschedule another event to occurr.
A simple list will not suffice if there are many events that are added and removed each pulse. First we can started by keeping the list sorted. Then each pulse, we simply remove the head of the list and execute it as long as the event there is to go off right now. However, with many events inserting will then be slow. The solution I have taken is a closed(open??) hashed table with N buckets: events that are to go off at time M will go into bucket M modulus N. E.g. assuming 64 hash buckets, the event that goes off at pulse 1 will go into the list pointed to by the bucket 1, the event that will go off at pulse 256 will go into the bucket 0, etc.
In case 64 buckets are not enough, it is then almost always helpful to just increase the size, to shorten the lists and thus the insert time. The events seldom go off at a specific time, if they are just sufficiently randomized, so there are no problems with them filling up certain hash buckets.
With a large amount of events, extraction of objects is a problem. If the above mentioned mob that moves around dies, we will have to extract the events that have him as a primary actor right away. Going through all the events and checking might be too slow. I keep therefore another next link field in the Event structure - just like the char_data has a next pointer to the next character in the global list and a next_in_room to the next character in the room.
This also comes in handy in other cases: I find that it's often useful to check if a character has an event of a certain type already attached or to remove all the events of a certain type on this character.
Currently available from here.
An introduction system is becoming an increasingly popular feature. With such system, players do not automatically see other player's name when they encounter then - instead, they see a short description like "a happy elf".
The system originated, I have been told, on the LP-MUD Genesis for quite a long time ago. The first MUD I encountered to use this system was Shattered Kingdoms - after playing there for just a few hours, I was determined to develop it on AR - it greatly enhances a Roleplaying-orientated MUD.
I decided to use a simple array for storing who a given person knows. Since comparing strings is slow and unwieldy, each player will be given a unique identification number when their character is created ( see also versioning section on how to do all this without making players start over). The key is generated by using the current time, making sure though that no keys are the same. Below code is not 100% secure - if you say, have 20 players create their characters in one second, then the MUD crashes and restarts in less than 20 seconds, and then a new character is created again, duplicate numbers might appear. Same might happen if the system clock is for some reason set back.
int get_unique_player_id() { static int last_player_id; |
The above code can also be used for generation of other ID numbers, e.g. notes.
My PC_DATA structure contains an int*, which points to a dynamically allocated block of memory containing as its first element, the number of entries in the array, and then that number of player ID numbers.
Luckily, most of the times that a short description of a player or mobile is required, the PERS() macros is used. Thus, you can simply replace this macro with a function that will given a onlooker and a target return this target's name. I personally added another parameter to the PERS function, one that would override introduction (but still show e.g. "someone" for invisible characters). This is useful for things like global channels or TELL (though how TELL and global channels should work is an issue you have to think through for yourself).
Whenever someone introduces themselves to you, the array is expanded with another element, containing the id number of the new acquaintance. For higher performance, this array could be sorted to make lookups quicker.
How should a player pick how he or she looks? Preferring a system which would not require any intervenience from administrators, I allow players to select a main appearance (e.g. "a bearded human") from some 80 different options ( which can easily be added to online). Genesis allowed you to completely customize the short and long descriptions. Unfortunately, in these times, I'm afraid that a large amount of an administrator's time would then be spent arguing with players that "A giant elf, ready to KILL YOU!" is not a valid description.
What happens after introduction? The simple system described in here reveals the character's true name. It could be easily extended, by changing the int id array to a structure, to save not only the ID name, but also the name under this person introduced himself by. This gives some interesting possibilities for posing as another person, yet does create some problems: What to show on TELL, global channels? To whom should notes be addressed?
Memory and CPU usage are negligible. Some of the veteran players of AR know about 350 people - with 100 of those veteran players online, the total memory used would be about 150 kilobyte (plus some memory manager overhead). Allowing for players to pick what they want to name the will increase this to about 700 kilobyte. Scanning through a 350-element array for a certain number also takes little time - even when it is unsorted, and if it is sorted, the number will be found, or not found in at most 9 (%%?) tries.
Below is some sample code. This will not necessarily compile. As you can see, the code involved is fairly simple. One thing to watch for is replacing the PERS() macro with a function that uses a static buffer: if the function is called a second time, the previous result is overwritten, so using it twice in the same line will have bad results. However, with a system of allocating static buffers that Oliver Jowett has implement for IMC, this ceases to be a concern. Note: when allocated for the first time, the memory array should contain one element, 1, which is the number of entries in the array, including the counter itself.
You might also want to take a look at Peter Vidler's similar document.
/* Does ch know victim? */ bool knows_char (CHAR_DATA *ch, CHAR_DATA *victim) { int i; if (IS_NPC(ch) || IS_NPC(victim) || IS_IMMORTAL(ch) || IS_IMMORTAL(victim)) return TRUE; |
By adding a single element to the area data and player data structure, a version number, you can quite easily accomplish what would otherwise have taken a pfile wipe or a time-consuming manual conversion. I have only seen this method used in Russ Taylor's ROM server, and there only for player files. Smaug also uses it.
Imagine for example, that you have just introduced a new combat system which is based on weapon proficiencies - how do you give existing player some fair amount in those new skills, based on perhaps what they carry around with them, while making sure they only receive such reimbursement once?
Simply, compare the player file version after loading it with your current player file version. If the player file has version 0 (e.g. you have just added versioning) you could then give the player the above described proficiencies, then set their version number to 1. Next time they login, the procedure will not be repeated.
This is where a fall-through switch becomes useful:
#define CURRENT_VERSION 4 |
Since there is no break at the end of each case, a version 0 character will have all the cases executed.
Versioning can be used in area files to extend the information stored there without breaking backwards compatibility with stock areas (which may or may not be a feature).
Almost all MERC derived MUDs used an OLC derived from The Isle's OLC. That OLC adds an extendable #AREADATA section, which is like the player files, based on keywords and values. Adding another Version keyword to that section should therefore be trivial.
Then, when reading in an area file you would typically do something like this:
if (last_area->version == 0) /* Last area is a global holding the current area */ { fread_number(fp); /* Read in some default values for backwards compatibility fread_number(fp); fread_number(fp); |
The above could be some example code used to get rid of those many useless legacy numbers (e.g. area number) currently saved in MERC area files, and adding a new element, FOO, that would be a string only read in in the new. The code also gives a fitting default value for FOO for the old areas that do not have that variable.
When saving the area, you would save it with all the latest features, and the highest version number. Backwards compatibility for saving areas is typically not wanted, but could also be worked in, by for example setting the area's version to the wanted version before saving it, and then in the area saving routines, saving elements that belong in this current version only.
Recently, I decided to rework Abandoned Reality to use C++ features. Many things can be gained from this. I will try to illustrate with example some things that can be made easier by using certain features of C++.
Is rewriting a C program in C++ a good idea? Most will agree that to fully use C++ features you would have to object-orientate your program from the start. Unfortunately, in AR's case, there are 160,000 lines of code and 15 megabytes of area/data files that cannot just wait while we start from scratch. I decided therefore to use C++ as just an enhanced C.
The first thing I did was to change the dynamic, crashproof buffers into a class. Code that previously looked like this:
BUFFER *buffer; |
was reduced to this:
|
Constructors let us initialize the buffer without having to explicitly call buffer_new function. Default values allow the buffer to start at the default of 1000 bytes. The destructor make it unnecessary to worry about freeing the buffer. Finally, a conversion operator (%% right name?) will allow us to get the text in the buffer - without exposing the data in the buffer to the outside.
This is a small example of how C++ becomes a better C with just minor changes.
Memory management. AR's memory management system was written by Oliver 'Nemon' Jowett and is publicly available from his web page. It allows for making effortless recycling of structures, using a macro-based approach. Adding the structure name to a table will generate both a free_structure and a new_structure method which will use a recycle list and allocate more memory as necessary. In addition, it is possible to preallocate a number of data structures, removing memory manager overhead.
Stuff about how the MM works now.
The two major free operating systems for PCs are FreeBSD and Linux. Plenty of information about these can be seen on those pages. Personally I prefer Linux, because of the greater user base and variety of already ported software. A typical installation will require 200 Megabyte of disk space. Almost any modern PC will do - 16 MB of RAM and a 486-33 will run it comfortably, but more RAM and a faster processor is of course better. It is probably best to buy a distribution rather than download the packages yourself - if you are not familiar with Unix already, several companies sell the CDs together with a book.
I cannot in any way recommend Windows for MUD development.
fte has all of the above and more, and is my favored editor. It runs only under Linux (Virtual Console or Xterm) and some other non-UNIX operating systems. Most useful features are: syntax highlighting, block operations using shift+arrow keys, full menus, ability to completely customize it - easily, parsing of compiler output (so you can jump to the next error in compilation).
emacs - is THE most powerful editor. There's a variant called XEmacs which has menus and more (but runs only under X). It otherwise runs on all platforms. It's difficult to start with - the key combinations are not easy to remember and can be hard to change.
joe - easy to use editor, with wordstar-like key layout. It was my preferred editor before I discovered fte. It runs on all platforms.
jed - an editor with a lot of features as well. Lies between joe and emacs on difficulty of use.
pico - not recommended. Although simple to use, it has none of the features that are needed for serious programming.
vim - a free vi clone. If you like the vi way of operating, it's for you, but most people prefer something simpler. It has all the features you need.
wpe - Looks much like Borland's IDE under DOS. I never could get it to work properly and found it clumsy, but that was a while ago.
Revision Control is a very useful thing to have. I have a separate document on using RCS for MUD development here.
Perl is very useful for doing complicated search and replace operations on large amounts of text. Replacing the thousands instances of "IS_SET(ch->some_flag, SOME_BIT)" with "ch->some_flag(SOME_BIT)" was quite easier with a one-line perl script than having to do it manually (or using an editor, since most do not support search replace on a list of files, but just the current window).
The C_ECHO snippets contain a great number of useful C code fragments as well as various references, like a list of vt100 codes.