Phantasmal MUD Lib for DGD

Phantasmal Site > DGD > Writing a Library > Persistent MUDLibs

Persistent MUD Libraries — Yes or No?

The DGD server, written by the excellent Dworkin, has changed a lot from its conceptual roots in LPMUD. The language is very different, the server features are very different. Its dialect of the LPC language is strongly based on the original LPC, and thus on C. But one of the visions that has driven some of DGD's most powerful and unusual features is persistence. Persistence makes a major difference in a MUD library, and you'll need to decide early on whether you want to take the effort to support it.

If you do decide to use persistence in a DGD MUDLib, the Kernel Library is worth a second look. It includes extra support for a lot of the issues that persistence can cause your MUD.

DGD's Definition of Persistence

You'll hear the phrase 'persistent MUD' batted around a lot, and 'persistent world' batted around even more often. It can mean any of several different things. The DGD community talks about DGD providing excellent support for a persistent MUD. What do they mean by it?

The phrases often refer to the game design. However, DGD can't dictate your game design. Persistent game design is a different topic.

Primarily, the DGD community means that the game runs as though it would never shut down, and as though the server never went down, even if the server will go down. DGD is said to be persistent because the entire state of the game in memory can be preserved when the server is rebooted. This allows the game to resume in exactly the same condition that it was in when the server shut down, including all internal variables of all objects. The game, as a single 'virtual run' of the program, never stops and rarely even pauses. DGD's definition of Persistence involves being able to keep actual LPC-language objects around, potentially forever. The object pointers never expire, you never have to save or restore, you never need to write code to back up your objects — because conceptually, the server never, never goes down.

Par Winzell probably explained it best:

It's almost impossible to convey the experience of running a truly persistent Mud to somebody who has never done it. So many assumptions disappear. The way objects are created, areas designed, everything changes.

In traditional LPMud, virtually everything is an initialization script, like the create() function in your example. Half the code in a wizard's directory is startup code. In a persistent world, rather than write a lot of startup code, you tend to write behaviour code, and configure objects.

Most code in a traditional LPMud isn't real code, it's just a cumbersome configuration (set_this and set_that) technique. That pretty much goes away in a persistent game.

The cumulative effect is that if you design your mudlib to be fully persistent from scratch, you will find yourself making subtly different decisions on pretty much every single design question that comes up, and you end up with something drastically different than LPMud.

DGD Support for Persistence

If you're never going to shut the server down, you need the ability to upgrade your LPC objects in place. Eventually you'll want to add new features, new content, new code. That means changing the code and data of existing LPC-language objects, and adding new code and data to them. Since you're never going to stop your current run of the program, you'll want to deploy all that immediately, preferably without even logging your players out. DGD allows you to do this.

One way DGD simulates a single continuous run of the program is the State Dump. A State Dump dumps a copy of all your LPC objects, whether in memory or swapped out, with all connections between them, to a file. You may then, later, start DGD from that State Dump file and it will pick up precisely where it left off. There are a (very) few things that necessarily change, such as the time and date, and what network connections are active. But mostly, a persistent DGD program can keep all objects around forever. When you restore from a dump, it just looks like the clock skipped, or like you took awhile to run the next thread of execution, and like all the network connections suddenly dropped. Effectively, that's what just happened.

Keeping a single run of the program going for so long requires a object cleanup solution. You need to be able to find old objects that are no longer in use and destruct them. Over days, weeks or years, an object leak will eventually bring your MUD down because old leaked objects will never go away. Rebooting no longer helps because all your old objects will stick around across reboots.

But the curse of eternal objects is also a disguised blessing. You can save a lot of code if you don't need to rebuild all your object structures when you reboot. You no longer need to write your data to disk. If you make a change in an object, it stays around even if you don't have a way to write that change to a data file. That can be a big, big change in the way you think about your MUD Library.

Normally, keeping all of your objects in RAM at all times would mean using a ridiculous amount of memory. However, DGD is a disk-based server, and believes very strongly in swapping things to disk constantly to save memory. This means that if you store data as 'in memory' LPC objects, they wind up taking no more memory or disk space than if you saved all your idle objects to disk. But you can access LPC objects in memory instantly, and DGD will transparently handle all swapping.

Persistence and Player Bodies

Persistence is also related to how you choose to represent the player in your MUD. Whether the player's body is represented by a single monolithic object or separated into a body and a connection object determines whether you can simply keep the player's body in memory when it's not connected. Note that you don't have to keep the body visible in-game to keep the body's LPC object stored somewhere in memory.

A monolithic object would contain the connection (originating IP address, the ability to send and receive data), but also all the player's information like skills, name, password, history and so on. In a non-MUD application, the equivalent would be user preferences and settings, interface code, and any avatar the user might have in the program. Separating out the connection-specific data (IP address, data transmission) allows you to store the persistent information like preferences and body data in a separate object, which (on a persistent server) never needs to be destructed, nor the settings saved to a file. That saves save⁄load code, as do many other similar situations.

Persistence also affects how equipment may be stored. A player's equipment can be kept with his body, including any customizations. The MUD would simply move it to an unused or virtual room, if the body persists anyway. Doing so is optional, of course, but it's another form of saving-to-file that can be avoided with a persistent MUD if the implementor so desires. If your MUD permits object customization, this can be a way to save it long-term.

You'll need to periodically delete unused players if you need to free up space, just like you'd have to with player files. You'll need to list all players and determine whether they've been idle so long that they need to be deleted. That's usually easier if the players are stored as in-memory LPC objects rather than needing to be parsed from data files.

Upgrading Existing Objects

A problem with upgrading in-place is that if you change certain parts of how your LPC objects work, you'll be left with a lot of LPC data in the old layout, designed for the old code. In a MUD with save and restore, it's possible to write a datafile adapter and kludge around this when you reboot. The equivalent for a persistent MUD is an update function.

To upgrade objects, you'll already need to have object management code to rebuild files. Your object manager can also call upgrade functions on master objects easily, and those master objects can track and upgrade clones. If you're using the Kernel Library, it's also possible to track clones by owner with the ObjRegD, or you can roll your own tracking system.

In any case, it's sometimes necessary to write code to initialize new fields (such as one you just added) in old objects, or move data from old fields into new ones. It's not harder than writing a datafile converter that does the same thing, but the results can be trickier if you do it wrong. Remember, in this case you're updating live data and you may have players connected at the time. Backing up first is a useful thing, and it may be useful to start a secondary 'test' server from a live statedump and try your upgrade code there first to make sure it works.

You may also find that it's useful to do an upgrade in stages. For instance, if you change the type and representation of a field in an LPC object, you may want to add the new one, put the data into its new location, then do a second LPC upgrade to remove the old field.

Example

For instance, I might change my objects to stop storing a "brief description" string and instead store offsets into my arrays of nouns and adjectives to make a description. So if you see a "broad green tabard" then you're guaranteed you can "look broad green tabard" since it uses nouns and adjectives in the object.

However, when I take add the new data field (let's say an array of integer offsets, so it'll be an int pointer), I'll need to fill in decent values. I could make it default to saying the first two adjectives, then the first noun. So I'd set the array to [0, 1, 0], where the last element is the offset into the array of nouns.

So far, so good. But where do I put the code to fill in all those fields? One answer is "in the ObjectD". My favorite answer is to have the ObjectD call a function called "upgraded" if it exists on every object that gets upgraded. That's what Phantasmal does.

So I could put that code I mentioned above into the upgraded() function of my object type, then type "%full_recompile" at the command line, and my objects would all start being described as "big brown table" and "flat checkered floor" and things, all according to the first nouns and adjectives on their current lists. Then I'd go fix all the descs, 'cause that would look funny :-)

But I've had to write specific upgrade code for the specific change I made to my objects. In a standard MUD, I'd make the change to the object loading code, then shutdown the MUD and restart it. Then all my objects would be appropriately altered, but I'd also have to drop everybody's network connection to do it, and generally disrupt things. Not nearly as convenient. Plus, it's slow to do all that loading and saving of LPC objects using LPC code. Much faster to use a statedump.

Why No Load and Save?

If you use statedumps, there's no reason to ever move things out of memory, usually. You'll need backups, but statedumps are (under DGD) the fastest available backups if you're writing out most of your MUD's data. That saves you having to write code for reading and writing objects, and more importantly for large interconnected systems of objects.

You may choose to have object load⁄save code anyway, probably for object import⁄export. Skotos and Phantasmal both do this. However, object saving is slow compared to statedumps, so normally object saving will only be used for export to a different MUD or different program. You can also use the traditional save-reboot-load hack for object upgrade, just like a non-persistent server has to do. That can be easier in some cases, at the cost of some downtime (and having to write all the load⁄save code!). That's basically how Phantasmal uses it.

Having the ability to read or write datafiles can also be a good means of object creation, or to import zones from other types of MUDs, or similar builder tricks. Outside of your MUD, everything is going to move around as files, so it can be good to read and write them.

Can't This All Be Done Without DGD?

Certainly. A shovel's not the only way to dig a hole - you can use a spoon, if you're so inclined. You can write a large amount of code to save and restore huge interconnected systems of objects. You can do the years (over a decade now) of testing that DGD has already received on those systems, and do it all for yourself. None of this is impossible — DGD itself is written in C, so obviously a C-based system will do all of this just fine if it's designed correctly.

Now: have you asked yourself why you'd ever want to?

From: DGD Mailing List (Par Winzell)
Date: Sun Apr  4 11:45:02 2004
Subject: [DGD] Re: Clones and very large arrays

>> ... and one kind of iterator that makes no such promise. Obviously the 
>> former kind has to either use more memory, or special tricks. If you 
>> don't mind the iterator using a fair bit of memory (temporarily) you 
>> can  simply let the iterator LWO make a private copy of the bigmap, 
>> keep two index counters (one to index the mapping of mappings, one to 
>> index the current inner mapping), and step through the whole thing 
>> step by step as next() is called.
> 
> I would have thought this method to be quicker than the find_object 
> method you described.

Depends on the number of objects. In a Skotos game, clones of 
/base/obj/thing constitute > 90% of the objects in the game, and it's 
pretty pointless to keep an explicit data structure around enumerating 
180,000 objects when you can just loop 200,000 times and get them all 
anyway.

> But you still use bigmaps to store the actual clones, right?

Nope. There's no point... for this particular problem, and for clonables 
whose clones constitute a significant portion of the objects in the 
game, find_object() happens to be all you need.

> So as I understand it, the iterator is just a way of retrieving the 
> contents of the datastructure?

Yes, although the emphasis should be on it being THE way; i.e. once you 
start using them, you quickly start taking it for granted that wherever 
there is a data structure, you can fetch its iterator. I can communicate 
these facts but I can't communicate fully the experience. These OO 
concepts seem to solve nothing new, but they change everything.


> And Bart mentioned that it is a useful method of getting rid of 'stray 
> clones'. Unfortunately I don't see how. If you have a 'stray clone' 
> lying around, how are you going to spot it from within a gigantic 
> datastructure?

Again this is one of those things you don't fully understand until you 
have had to administer a persistent game. Once you boot DGD into a game 
that uses state dumps, you will never ever again cold start it. That 
means every persistent resource (i.e. rooted in a persistent object) you 
create will still be with you in the year 2350 unless you have, on th 
very first day of your uptime, administrative code in place that keeps 
track of these objects.

If you have a guest programmer who just executes,

   clone_object("/foo");

without doing something with the return value, and you don't have code 
at a low level that registers this clone somewhere, this clone is more 
or less gone forever. It is never garbage collected (it is not a LWO) 
and nobody has a pointer to it. The only way it can be found is by using 
find_object(), and for that you have to know the path name.

Once you realize what a potentially huge and dark haystack an eternal 
uptime is, and how little of a safetynet there is unless you write one 
yourself, you will see the reason why people here rail against the idea 
of rewriting the kernel library from scratch. There should be a layer of 
code in place on top of DGD and below your mudlib that has been tested 
by dozens of people before you, with all the bugs ironed out, that you 
can rely on with absolute certainty. From the perspective of the mudlib 
writer, such administrative code should be considered more or less part 
of the driver. This discipline is essential.

> Also, as people left me with the impression that they don't often recall 
> the entire list of clones (those who use linked lists), what kind of 
> useful information are they deriving from it?

It's there for the same reason you keep 10 gallons of water in your 
basement for when the earthquake hits, yet you don't amble down there to 
refill your glass during dinner.

Zell