The Natural Way to Handle Commands

by Scatter ///\oo/\\\


This document was written by Scatter. Since the page that originally hosted it seems to be done, I pulled this out of Google's cache. If the page appears elsewhere by Scatter or asks for me to remove it I will do so.
Recent versions of MudOS contain within its mystic realms of options and packages, a system known as a natural language parser. Argued by some to be one of the foremost features of MudOS over other LP drivers, this system provides a powerful alternative for handling user commands within the mud. So, what is it, exactly? Why is it better than the older ways?

The old way

In the beginning, there was add_action(). The only way to make a command available to a player was to have an object add the command to the player via the add_action() efun.

This means to be able to use a command, either the room the player is in, an object in the room, an object in the player's inventory or the player object itself must add the command with add_action().

The code to process the command and carry out the desired action goes into an accompanying function in the object whose name is specified in the add_action() call.

If more than one object happen to add the same command to the player, then the processing functions will be called in turn until one succeeds. Failure messages are set by other efuns such as notify_fail() and if none of the objects successfully processes the command, the last failure message to be set will be displayed to the player.

Problems...

Here is where the problems of add_action() start to make themselves felt. When several objects contain the same command, the order in which the driver will try them is unspecified. An object handling the command has no way to know what message will eventually be shown in the event that they all fail. It can set its own message but has no guarentee this is what the player will see. It also means that if more than one object would have processed the command successfully, there is no way to know which of them will be tried first. In addition, it could be that the one that the player had intended could fail, and an unintended one subsequently succeed giving the player an entirely unexpected and wrong result.

To illustrate, suppose you are carrying two potions. Both are drinkable, and so both will use add_action() to add the "drink" command. One of them is fatal. The other will heal you to maximum health.

If you type 'drink potion', will you live or die? This is entirely arbitrary and may depend solely on which order you picked the potions up in.

Suppose the health potion is at the top of the list and gets processed first - you might think you are safe to type 'drink potion' because the health potion gets first shot at the command. But what if the health potion has a stopper that prevents you drinking it? The health potion will set it's error message 'The stopper prevents you,' and report to the driver that the command failed. The failure means the other object is tried - the fatal potion has no stopper so handles 'drink potion' successfully...

This kind of problem is often compounded by objects in the room defining the same command as objects in the player's inventory thus confusing players as the unintended object is picked first. Plus there may be objects that are written badly and do not report failure correctly.

As an example of the latter, suppose you are carrying a book that you want to read, and another book which happens to be blank. Suppose the blank book gets picked to handle the 'read' command first, but instead of setting a failure message 'The book is blank.' and reporting failure so that the command is passed to the other book, it instead just uses write() to display the message and reports success. There is now no way to read the book that actually has writing in it, without juggling items around in an attempt to get the other processed first.

There are other problems with add_action() too.

When add_action() is used, it specifies a function to handle the command. When the command is used, this function gets called with the text the user typed, less the initial command. So for example, 'read book' might result in do_read( "book" ) being called in the book object.

This means every object that defines a command must work out what the rest of what the player typed means. For example, suppose there is a plaque in a room with writing on, so the room object defines the 'read' command to let people read it. The player is also carrying a book, which also defines the 'read' command.

If the player types 'read book', both the room and the book will have their do_read() functions called with 'book' as the rest of the string. That is to say, the add_action() system has no concept of the fact that one of the objects handling the command is a book and one isn't - it'll try both of them anyway. This means the do_read() function in the room will have to check if the player typed 'plaque' and fail if not so that the book gets a shot at the command.

This means the coder of the command must do a lot of work in working out what the player meant and whether the command refers to it or not. That book object must determine that the player actually meant to read that specific book before it can do anything other than fail. So, the code must check the string passed and see if what the player typed identifies this specific book.

This can be a lot of work, even with a simple command like 'read'. What if the player typed 'read blue book'? Or 'read first book', 'read tome', 'read spellbook' - even 'read the book' or 'read my book'. The coder effectively must guess all the possible options the player might choose and account for them all. More complicated commands can easily make this a nightmare.

Another problem with add_action() arises from the way MudOS clones objects. If you change the file an object is cloned from, the changes will only affect future objects created from that file - existing ones retain the older code. So, if you update the read command in a generic book object then for the changes to take effect you must destruct every book object in the mud and clone new ones.

With pervasive objects like weapons, armours, clothes and containers this quickly becomes impossible, meaning you either let the change trickle in as objects are naturally destructed and recloned or you go for a full shutdown to get your change in game.

Many muds define a large number of commands in the player object itself - this means any change to any of the commands requires you to boot all your players off the mud so that the player objects are destructed and re-cloned.

Another problem with the traditional add_action() scheme is that commands are only available when objects are present that define them. As a result I might be able to 'read' in a room with a sign, but not in another, empty room. In the former a player might get an error message 'Read what?' or similar if they were to type 'read book'. In the other, they would simply get the mud's default error message - often 'What?'. This can lead to confusion as to what commands are possible, especially amongst new players. They try things that make sense to them, get a 'What?' response and assume the action is not possible anywhere.

On top of this, the same command may work completely differently in different places as different coders will tend to write their commands in different ways. In one place you might have to type 'fill bucket from fountain' whereas in another it might be 'fill bucket with water' to do the same job. In another, either may work. Sometimes a 'fill' command on the bucket may override a 'fill' command in the fountain, preventing things working, or vice versa.

Development and enhancements

Over the years, many mudlibs have tried to deal with the problems with add_action() in a number of ways.

A typical way is to make the most common commands global - either by catching player commands after add_action() processing has finished or by extending the add_action() system to allow wildcards. This lets you add a command "*" for example in the player object that will match anything the player types. The text can be manually examined to find the first word and specific objects called to deal with the command. For example, 'read book' might result in "/commands/player/read.c" being called to handle it.

This gives you one object to update instead of many and you can update it without bothering players at all. It can also help to solve the error message and object ordering problem, though if normal add_action() commands are used in addition to it, then there is still the possibility of the global command being overridden by a command in an object.

What it doesn't help with is parsing the text to find the player's meaning. If anything, it complicates this because now the object must handle all possibilities for all objects instead of just for one specific object. In addition it has to work out for itself which objects are possible for the command - a chore that didn't exist before since any object for which the command was possible handled the command itself.

Some mudlibs have developed systems such as add_command() to replace add_action() - these try to make all commands run through a specified syntax and parses all commands in a generic way. This helps and avoids many of the problems with add_action() but often there are still problems with flexibility, with error messages and with objects defining the commands needing to be present.

Another problem with these kinds of enhancements is that they take processing out of the driver and into the mudlib. This can be bad because on a big, busy mud it is often necessary to optimise as much as possible to reduce lag. Commands are frequently typed on muds and processing them in LPC through mudlib enhancements is going to be much slower than letting the highly optimised, compiled driver handle them.

The natural way

As well as the asd_action() system, MudOS provides an alternative method of handling commands - the natural language parsing package.

The idea is simple. You have a set of objects that define the commands. A command object defines a series of grammar rules which the command will accept (for example 'give OBJ to LIV') and has a set of functions for each rule which tell the driver whether the rule is possible and carry out the command for each rule. Player commands are simply sent to the driver using the parse_sentence() efun.

The driver now examines the text the player typed and works out not only what the command was but what all the objects referred to within it are. It generates error messages if necessary or if it matches a rule it calls the appropriate function in the appropriate command object.

Here, a crucial difference from add_action() appears - instead of passing the handling function the string of text that the player typed, the function is passed the results of parsing that text. That is, the function is called with the objects referred to and the names they were referred to by, instead of the raw text.

So when you write a command, you don't have to worry about whether the player typed 'book', 'the book', 'blue book' or 'my second red tome' - the driver worries about that. You just get handed the book object that is being referred to.

Your parsing worries are over. No longer do you have to try and guess all the different ways a player might specify something - you just give the command the rules it needs and the driver worries about the rest.

The next advantage of the parsing package is with error messages. It can be tricky to set this up, but done well it means you never get the wrong error message again. There are two parts to the error message system - automatically generated error messages that happen when parsing is unable to match the grammar rules, and error messages specific to objects.

The first kind occurs when the parser is unable to match objects to what the player has typed, or when objects are matched but are the wrong type. Taking for example the rule 'give OBJ to LIV' and supposing the player typed 'give my second sword to the blue statue'. To match this rule, the parser will try and find an object that matches 'my second sword' and a living person who matches 'the blue statue'.

If the player doesn't have two swords, the parser would automatically generate a message like 'There is no second sword.' Similarly, the statue is not living, which might generate a message like 'You cannot give the second sword to that.' These messages are generated without you even having to think about them when you code the command.

The second kind of message are generated by the objects being referred to. When the driver matches an object to something the player types, it effectively asks the object 'Is it ok to do this to you?' The object replies 'yes', 'no' or gives an error message to explain why not.

These messages you do need to code yourself - you add a function to each object for each command rule that generates them. If you don't add a function for a rule, then the driver assumes that the rule cannot be used on the object. In this case an error message is generated along the lines of 'You cannot do that to that.' This system lets you specify things like not being able to put things in containers that are closed or full, or into things that are not containers.

Seperating these checks and their error messages from the command object itself gives several valuable rewards.

Firstly, your command processing function does not have to do any checking that the command is possible on the object it has been given - if it has been passed the object then if is definitely possible. So the code can free itself of checking things and producing error messages.

Secondly, behaviour specific to a certain object is generally kept within that object's code, and special cases for special objects are kept within the special object concerned. So that magic sword that can't be drawn from the scabbard unless your name is Barbarian Bill needs no special handling in the 'draw' command at all - the extra check stays in the special sword.

Thirdly, it prevents the driver selecting inappropriate objects when matching some text. For example, suppose there are three hats in a room but the second is hidden from view. A player types 'get second hat' - obviously the player is going to mean the second hat that he can see, not the one he can't. If the checks were in the command object the parser would have to return the hidden hat to the command object, and the command object would then be forced to generate a message like 'You cannot see the second hat.' With the checks in the object, the driver can ask the object if it can be used with 'get' and as the second object returns 'no', the third object becomes the second usable object and so the right object is passed to the command.

The natural language parser requires quite a bit of work when initially writing commands - it can be tricky to make sure all the parts work together to make sure the right error message is always given in all circumstances. There is a huge bonus that more than makes up for this - how easy it becomes to code objects that existing commands operate on.

Generally, the code that says 'yes you can use the xx command on this object' can be contained in an inherited module. The existing command object is written to call a named function in the object to actually carry out the command. So to make an object respond to the 'pull' command can be as simple as inherit "/std/modules/pull" add adding a do_pull() function that carries out the results of pulling it.

This setup means that the coder of the object has no need to worry about what the player typed and whether or not it matches the object; no need to worry about error messages, whether or not it will be overridden by other objects and whether or not the command should mark itself failed or succeeded. The only thing that needs to be coded is the bit that does the pulling.

Quality

From an administration point of view, quality control becomes a lot easier with the natural language parser.

All your commands are available anywhere, so you guarentee that the same command will work the same way in every case, that the way you invoke it doesn't change from place to place, and that you won't need a different command to do the same thing somewhere else. All possible commands can be made known to the players, eliminating the frustrating 'syntax quest' to guess the command that does what you want in this particular room.

It becomes impossible for badly written objects to make other objects seem to be broken. If an add_action() command doesn't report failure correctly, it can prevent perfectly good commands in other objects from working and tracking down why something works in one circumstance but not in a different one can be very time consuming. With the driver's parser, this just can't happen.

A related issue is that object ordering becomes unimportant because there is only one object handling the command. By default if the parser encounters a reference that it can't resolve, e.g. 'drink potion' with two potions carried, an error message is generated - 'Which potion do you mean?'. If the player really doesn't care which, they can type 'drink any potion' and the parser will pick one of them arbitrarily.

(It's worth noting that many people find this unfriendly, and the parser does have an option to automatically use the first match instead of generating an error message. The problem with this approach is that even if the player knows the first potion is the one they want, they can end up in trouble if someone else hands them a new potion before their command goes through - or if they didn't realise that that bottle they are carrying can also be referred to as a potion...)

Finally, along QA lines, you don't have to worry about coders remembering to check for all possible ways a player might reference an object - the driver takes care of that for you. You don't need to worry about coders forgetting to check if objects are hidden, invisible, in a closed container - the inherited modules carry all that code with them.

No pain, no gain

Setting up a command system using the natural language parsing package does involve quite a bit of work and can be frustrating at times. Once done, though, it pays off big in several ways: reduced coding time needed for game objects that need commands; reduced teaching time needed for new coders learning how to make their objects respond to commands; reduced maintenance time needed as there less to go wrong; error messages that are always correct; and the commands themselves can be massively more flexible in terms of text they can accept and understand.


Scatter has also written a guide to using the MudOS natural language parser. His spider logo ///\oo/\\\ is a hangover from a previous mud identity that just kinda stuck.

© Copyright 1998 by Scatter. This article was first published in October 1998 Imaginary Realities, the magazine of your mind.