My intention in this document is to roughly sketch the internals of TeenyMUD, particularly the database management code. If you don't plan to hack at the server at all, or if you don't know C, don't bother reading this. Database Implementation ----------------------- The database is implemented as an index, a cache and a chunks file providing backing store for the cache. The index is implemented as a large array (main_index) of pointers to 'descriptor' structs (things of type struct dsc). An object number is used as an index into this array to locate the unique descriptor associated with the object. A NULL pointer in the array indicates a non-existant object (typically one that has been recycled). The descriptor contains the flags, a pointer to the name, and the NEXT field (used in contents/exits lists) of the object it describes, as well as a pointer to where the remainder of the object data can be found. If the object is in cache, this is a memory pointer to an 'obj_data' struct, otherwise this is an offset into the chunks file. The flags field contains a flag (the IN_MEMORY flag) which allows the system to differentiate between these cases. Quite incidentally, the same descriptor structs are used to describe free chunks in the chunkfile, and a list of these is used to maintain data about the free chunks. Descriptors are permanently resident in memory, thus the name, flags and NEXT field of any object are rapidly accessible. This may or may not have been a good design decision, but is the way things are. All the rest of the data associated with an object is subject to being cached and paged in and out. When an object is read in from disk, the information about where it is on disk (the disk chunk it lives in) is copied into the obj_data struct, and the descriptor no longer contains this data. When the object is flushed from cache, if it is unchanged nothing happens, otherwise this disk chunk is freed, and then a new chunk is requested (since the object may have changed size while in memory) into which to write the updated object data. The descriptor is then filled back in with data about where in the chunks file the object data can be found. At any given moment, the descriptor conatins the actual size the object takes (or will take when written back) on disk. It is this number that is used for cache usage estimates (thus, cache usage is really only approximate, since in memory size is somewhat different). This size data is also used by disk_freeze() when requesting a disk chunk. The code responsible for reading and writing objects to the chunks file is in db/disk.c, and consists primarily of two routines, disk_thaw() and disk_freeze(), each of which accepts a pointer to a descriptor. The former reads the object data off disk into an obj_data struct, and returns a pointer to this, the latter writes an obj_data struct to disk, and frees all the associated memory. The code responsible for managing the cache -- inserting things into it, deleteing things from it, trimming it back to the current cache size, and purging it for dumps, lives in (surprise!) db/cache.c. Since it seemed like a good idea at the time, the code which keeps track of where free chunks ('holes') in the chunks file also lives in here. Routines for making a disk chunk free, and for acquiring a disk chunk to write data in to exist, and are used by disk.c. These routines implement a worst fit algorithm, and do amalgamate adjacent free chunks, so fragmentation should remain fairly well controlled. See my remarks in doc/teeny.doc for how to de-frag a database. For performance reasons, and since I didn't want to constantly maintain the index on disk, the cache is NOT write-through. Thus, in the event of a server crash, the chunks file is out of date, and the information about where things are in it is completely lost. The option is to write objects back to disk immediately when they are changed, and to update the disk copy of the index at the same time. I deemed this an unacceptable performance hit. Finally, the code for actually implementing the TeenyMUD database lives in db/db.c. It includes a suite of routines for accessing elements of an object completely transparently: set_str_elt(), set_int_elt(), set_lock_elt(), get_str_elt(), get_int_elt(), get_lock_elt(). These accept an element code, and return or set the appropriate element. They take care of allocating and freeing memory for everything, with the exception of set_lock_elt(), which expects to be supplied with a lock written into memory it can use (this is an anachronism, for which I apologise). Note that get_str_elt() and get_lock_elt() return pointers to the literal in-memory data. If subsequent database accesses cause this data to be purged and written back to disk, these pointers will be garbage. For this reason, the command handler calls cache_lock() to prevent *any* data from being purged from the cache at the beginning of each command, and calls cache_unlock() at the end. (and calls cache_trim() at that time to trim the cache back if required). Several utility routines are also provided, create_obj(), destroy_obj(), exists_object() and so on. The main index is read in at server startup time by read_descriptors(), and is written back during dumps by write_descriptors(). These functions read and write all the descriptors and descriptor data in to the index file, followed by a list of data about the free chunks in the chunk file. To bring the db into synch, one needs merely to flush the cache, and then call write_descriptors(). Note that doing this in the wrong order would be disasterous, since many of the descriptors written will contain data on where *in memory* the object data can be found, with no mention of the location in the chunk file. Game Database Layout -------------------- Each thing in the database has a number of elements associated with it. The usual strings (name/suc/osuc/fail/ofail/desc) and the usual integers (pennies/owner/location/home/flags) are present, and do the usual things. Here, usual means 'usual in the sense of TinyMUD.' Each object also has a contents element, and an exits element. These both point to the first thing in the exits or contents list of the thing, lists are constructed by chaining items together with the 'next' element. This closely resembles the TinyMUD database, with the difference that everything has an exits list. In particular, when a player picks up an exit, it goes into the player's *exits* list, not into their contents list. It is fundamentally this that makes the TinyMUD dump format incompatible with the TeenyMUD text dump. Besides this, there is nothing terribly exciting about the database layout. The astute reader will notice, however, the distinct lack of a password field on things. This is because most things don't have passwords, so I chose to code the passwords as the second word of the player name, and then be careful about things. The is responsible for much ookey code in the server, and I sometimes regret this descision. Such is life. TeenyMUD 1.1 also adds (optionally -- define TIMESTAMPS in includes/teeny.h) a timestamps field to each object. This is an integer, intended to be interpreted as the time, in *minutes* after 00:00 GMT Jan. 1, 1970, when the object was last 'used'. The database manager (described in doc/dbm.doc) can use these fields. Version 1.1 does not, however, fully implement timestamping. I added this stuff here to avoid obseleting databases in future, if I ever get around to properly doing timestamping. Actually, it's pretty easy to implement. I did write a stamp(<object number>) function, which lives in mud/utils.c, to write a timestamp (of the current time) on an object. Placing suitable calls to this function willy-nilly in the code will implement timestamping. I didn't do this because it's a) tedious, and b) not so easy to figure out where the 'right' places to call stamp() are. Network interface ----------------- The network interface is implemented in mud/tcpio.c and mud/misc.c (the latter is so named because I couldn't think of anything better). These implement a fairly simple server interface with minimal protection for the server. tcpio.c is a hacked version of the UberMud network interface, by the way. Many thanks are due Marcus Ranum for writing this beautiful code, which is much less beautiful since I've gotten ahold of it. The code in mud/misc.c implements command quotas on a per time slice basis, and is responsible for slicing input from the network up into commands and dispatching them appropriately. It is in mud/misc.c that WHO and QUIT are handled, as well as connect and create (create player). The function match_who(), for matching against the WHO list also lives in mud/misc.c, since it needs to fiddle around with data structures private to this module. Other stuff ----------- mud/match.c contains the bulk of the routines for resolving strings into database references. The code herein is somewhat ookey. Feel free to re-write it. See the comments for how to use it, it's fairly straighforward with a few icky bits that may or may not be well described in the code. The TeenyMUD command set is implemented in mud/cmds.c, mud/buildcmds.c, mud/speech.c and mud/wiz.c. Utilities used by these commands for doing all sorts of things are contained in mud/cmdutils.c. The command parser (such as it is) is in mud/command.c, as well as the code implementing the 'gripe' command. Look at this last, you'll like it. mud/boolexp.c contains, basically, three routines, one for parsing Boolean expresions into my nifty internal format, one for writing an infix (text) version of an internal-format Boolean expression into a buffer, and one for evaluating a internal-format Boolean expression. The internal format consists of an array of integers, the first of which is how many integers follow. The remaining integers constitute a simple RPN 'program' for the evaluator. Positive numbers are interpreted as db references, and evaluate to TRUE if the player is, or is carrying the numbered object. Negative numbers are operations, AND, OR, NOT and STOP. A small stack machine in islocked() executes the program, and returns the negation of the result. An artifact of this design (and of my laziness) is that when an object is recycled, any lock it appears in retains the reference to that object number. If and when the object number is re-used, the lock will now reference that new object. The alternative is to scan the entire db (sucking most of it off disk) and fiddle wildly with locks on everything. Ick. mud/teeny.c is the mainline, and also contains a few other high-level take-this-goo-and-make-it-a-MUD type routines, like dump_db(), the console handler and so on. mud/notify.c contains some routines for telling players and groups of players things. Hacking the server ------------------ Feel free to do so. You can do anything the copyright says you can, and that's most everything. To do some of the basic things people are *bound* to want, follow the instrctions below. + pronoun substitution All suc/osucc/fail/ofail strings are handled, in the bitter end, by do_strings() in cmdutils.c. Add a couple bits to the player flags in teeny.h (there's lots of room for expansion), add a couple more lines to do_set() in mud/cmds.c to allow the player to mess with these bits, and add a few more lines to do_strings() to substitute pronouns. Piece of cake, take you maybe an hour or two. + new strings (drop/odrop-like) This is trickier. You will need to extend the obj_data struct in includes/db.h, and modify disk_freeze() and disk_thaw() to cope with these extra elements. Then you need to define a couple more element codes in includes/teeny.h, and teach get_str_elt() and set_str_elt() what they mean. Finally you have to modify the appropriate player commands (do_drop(), do_kill() etc.. ) in the appropriate modules to echo these strings suitably whacked up at the appropriate times. + new data elements of other types Integers are easy. Just like strings, really, but teach get_int_elt() and set_int_elt() instead. Other data types, like p-lists, are probably a very bad idea. Don't even think about it. + modifying the command set Muck 1.0-like extensions would be cute, and not very hard to do, I think. Peer at the code in mud/cmds.c _et_al_ until you understand it, and then just do it. It is an unfortunate feature of my database interface that programming player commands is even more grotesquely tedious than you can imagine. Extensions like @find are not easy to do, since they require, basically, thrashing the entire database, most of which will typically be on disk. If you think a wiz-@find is bad on a TinyMUD, just have a shot at implementing it in Teeny. This is one reason that NO contents are displayed in a DARK room -- if objects controlled by the player were shown, then every time any player entered Limbo, every single object there would have to be unfrozen, to see if the player controlled it. Not good. Therefore not implemented. This sort of functionality is, however, provided in the standalone db manager. See doc/dbm.doc. Extending @rec to rooms is similarly sticky. The code for doing so exists almost entirely, except for the bit about unlinking all the database elements referencing the room (exits leading in, objects homed to the room). This, again, requires thrashing the entire DB checking dests and home. In this case, a check of flags will elminate the need to unfreeze the rooms, at least. Again, this is handled by the database manager. Have fun with it, folks... Andrew/bob