20080527

Commands

This post is going to document in prose how the command and interpreting system currently works. This was one of the early parts of the MUD I wrote, since it's a little hard to interact with the game without it being able to understand what you're trying to say. Along the way, I might even notice some bugs or shortcomings that ought to be fixed.

The easiest way to do this thoroughly is to follow a command from the beginning to the end of its lifetime. Let's start.

A human being types in a command into this telnet client. Let's say it's the "say" command. He types, "say hi".

There's a NetworkConnection instance looping through every line (newline-delimited) that comes in and passing it off to a ConnectionManager instance. The ConnectionManager ties a network connection to an entity in the game. In a way, it's the "brain" of a character, which controls it in the game world. Likewise, a NipicManager performs the same function for a Nipic in the game. You could imagine that the logic behind mobprogs would live in the NipicManager.

The manager passes that line of text to something called an InteractionMode. Since there are various ways that a player interacts with the world, the interaction mode encapsulates the interpretation of commands and displaying of output in those modes. For instance, most of the time, for example, when a character is moving around the world, the player is in the MainInteractionMode; but when he starts to edit his character's description, he's in the TextDocumentInteractionMode. Currently there's also a LoginInteractionMode and a CreationInteractionMode.

The interaction mode employs an Interpreter to parse the line of text received from the client into a Command object that contains information about the command and its arguments. Actually, the interaction mode receives an Invocation object that contains the Command object in question and its arguments. The Command is ignorant of the arguments being passed to it at runtime. Now that I think about it, I'm not really sure why Invocations need to exist. Instances of Command objects should be roughly equivalent to Invocations.

Anyway, the interaction mode in this case receives an Invocation object that contains a SayCommand and the argument, "hi". Now comes the fun stuff. The invocation is not immediately executed, but rather is placed in a queue of invocations. Invocations are picked off of the front of the queue on a periodic basis and executed.

The period is determined by the main loop of the MUD. Every manager, when connected to a valid network connection, signs up to be a PulseListener, which just means it implements the pulse() method. Every so often--to give you a frame of reference, SecondsPerPulse = 0.1--the main loop of the MUD calls pulse on all subscribed PulseListeners. The manager uses this call to ask the interaction mode to manage its queue.

The interaction mode does some processing and decides whether or not to execute an invocation. The reason all of this queue nonsense exists is simply because different commands have different amounts of "lag" associated with them. For instance, by default, every command has a delay of 1 pulse, but some commands deserve a larger delay. As a classic example, if you flee from battle, typically there is a delay of a couple seconds before you can continue running. In terms of what I just told you, for the next 20 times its pulse method was called, it would count down the time before the next invocation could be picked off the queue and executed. Note that the delay from a command comes into effect after the invocation is executed.

Once the invocation is finally executed, that's the end of the command's lifetime.

It's worth pointing out a couple other neat features of the interaction mode. The first is called a "fast" command. Certain commands, e.g. OOC (out-of-character) ones, should be exempt from lag that is preventing further commands in the queue from executing. The interaction mode's queue management takes into account fast commands, which are currently marked by having a delay of 0. Even if lag is halting the queue, fast commands are immediately executed. For now, the "emote" command is a fast command, because I believe that even if you're lagged, you should be allowed to type in-character text about what's going on with your character; it's a roleplay tool.

Another neat feature is called "flushing," which is something every good queue should have. Say you type in a series of commands with a long delay, but something happens in the middle--for instance, you get attacked--that causes you to want to cancel all those long commands you have queued. The interpreter, which is called immediately whenever a line of text comes in, immediately calls a method to flush the queue of the interaction mode, so the rest of those commands are short-circuited. Of course, it doesn't get you out of the lag from the last command. And since it doesn't hold the queue empty, you can immediately start filling it with a new series of commands that will start executing as soon as the lag clears.

In the previous description, I mentioned that the interpreter calls a method on the interaction mode to flush the queue. I just noticed that this design could probably be better. By leveraging fast commands, I could eliminate the need for the interpreter to have any knowledge of interaction modes. Less knowledge is better, as a general rule. I've made a note to investigate changing that design.

In terms of future work, there are two big work items: implementing the "!" command, which executes the last-typed command; and implementing aliases that are full-featured and help players equalize differences in typing speed. I'll be sure to post on both of these when I put them in.

20080523

How to Load Nothing

I ran into a problem with loading the other day that I felt was a shortcoming of the design. I found a situation where I needed to load from the data file the fact that a particular property of something is empty or nil. Really, this can happen whenever a property is optional.

In this particular case, the optional property was the short description of a character. Although Nipics tend to have short descriptions all the time, since there's no other real way to refer to them--having no name, and all--characters are usually referenced by their name in lieu of a short description.

None of that really matters. What matters is there was an optional property, and a shortcoming of the way loading was designed made me have to go through contortions in other places to support it. I decided, finally, to fix the design, because contortions don't scale well.

As you may recall from previous posts, there is some intermediate state during which an object is being loaded from the data file. During this time, the object is semi-initialized, and the process of loading keeps track of what parts have been initialized and which parts have not yet. Usually, those parts can't yet be initialized because they're waiting on some data on which they are dependent.

The problem with the design--and any CS people reading this will immediately agree--is that the unloaded state of a property of some object was represented by the nil value. What this effectively meant is that the nil value itself could never be loaded from a data file; every piece of data needed to be some real, concrete object, be it an empty string or some other kind of placeholder.

The way I fixed this is by instead using some arbitrary value to represent the not-yet-loaded state. As an implementation detail, I decided to use a Symbol, probably because it's very unlikely that I'll ever have a symbol being loaded from a data file. Consequently, I updated every single place where I made the assumption (implicitly, sometimes!) that nil represents a not-loaded value, and replaced the check or value or whatever with this symbol.

This was really quite painful, due mainly to the parenthetical above: there were many implicit assumptions, and most of them were hard to nail down. Aside from now supporting optional properties very easily, the upside of this change is that now there's no way for me to make that kind of assumption implicitly anymore. If I'm expecting or handling a not-loaded state, it jumps out into my face. In a way, it's a step towards making wrong code look obviously wrong. Perhaps more importantly, it's purged this automatic link in my brain between nil and not-loaded.

20080516

Eventos - a little taste of what may be to come

Aside from fixing tons of bugs with various things, I've been working slowly on Eventos, which are events, as you might have guessed. I won't say much about them now because the design is still somewhat in flux. It's basically an observer pattern that will replace each character writing text to other characters' terminals, in favor of sending messages to them. This will hopefully be the infrastructure that ultimately enables mobprogs, oprogs, rprogs, and whatever other kinds of progs you can think of.

A sample:

<type commands for a list> say hi
You say, 'hi'

<type commands for a list>
a man says, 'What do you mean, "hi"???'

<type commands for a list> say wut
You say, 'wut'

<type commands for a list>
a man says, 'What do you mean, "wut"???'

Yes, this gets old real fast. But it demonstrates something. Assuming the man standing in the room isn't parsing the text, "You say, '[text]'"1, he must be getting the text being said some other way. Indeed, he's receiving a SayEvento with the saidText property properly set to the contents of the message.

For fun, here's the old code...

@room.bagMobsInRoom.msgToAllExcept(self, "\\\\r\\\\n#{self.shortDescription} says, '#{text}'", "You say, '#{text}'")

...and the new code.

@room.speakEvento(SayEvento.new(self, text))

More on this later, when it's completed.

1 I would sooner stab myself in the eye than design a feature like that.