Natural Command Handling
by Scatter ///\oo/\\\
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 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 add_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 written a
guide to using the MudOS
parser and can be reached at
scatter@thevortex.com.
His spider logo ///\oo/\\\ is a hangover from a
previous mud identity that just kinda stuck.
The image used
in this article is an image of a band called 'Scatter the mud' and is copyright
© 1997 by Grant Hutchinson, used with permission.
October 1998 Imaginary Realities, the magazine of your mind.
© Copyright Information
|