20090529

Even More Controlling

The post name is a reference to an older post on a similar topic. In that post, I describe the design of snooping and controlling, two interesting things I believe a MUD ought to support, even if it's not used much in practice or only restricted to imms. In this post, I take the feature in another direction and implement "ordering", which is universally used, mainly with pets.

Briefly, for background info: the "order" command on a MUD allows you to issue an arbitrary command to another player or NPC. The commands you can issue are typically formatted exactly the same as the ones you might type in yourself. The ability to use this power, not surprisingly, is generally highly restricted. Imms can use order to command mobs during scripted quests. Mages can usually enchant lower level mobs and command them to fight or act. Players who have pets can command their pets.

There are a few potentially distinct problems here. Firstly, we need to be able to convey the command to the target in a way he will understand. Secondly, we have to decide on how the target will interpret it. Thirdly, we have to ensure that the target cannot execute commands that they aren't "supposed" to. Fourthly, the target needs to able to handle the delays associated with executing different commands. Finally, we need to ensure that the target cannot escape from an order in some unintended way.

Communicating orders - OrderEvento

Making use of the "evento" system I put in before, to convey an order to a target, the caller creates an OrderEvento object and sends it to the target. Actually, he broadcasts it to the room, because I feel that bystanders should be able to observe this. The evento contains a field with the exact text tat the caller typed in for the order. Here's what it looks like.

<type commands for a list> order man say what's up
You bend a man to your will

<type commands for a list>
a man says, 'what's up'

Interpreting orders

Once the target has received it, he needs to process it. This starts in receiveEvento, which is called for each recipient of each evento. The code detects what type of evento it is and invokes a handler. In this case, the handler takes the full text of the command and has it executed.

The question of how this execution should take place weighed on me for quite a while. One way I might do it is to have the sender (not the recipient) transate the command into an actual Invocation object that contains the command being invoked and the arguments to it. But this could allow the target to receive and execute a command that they normally wouldn't be capable of executing. The target would then queue it up to be interleaved with whatever commands his player is typing in. There's already queueing functionality in the InteractionMode; why would I want to duplicate it for the mob as well?

Well, I wouldn't. Instead I just reused the interaction mode for both local and remote commands. Recall that the interaction mode presents the player with a view of the world, mainly including commands he can type and what he can see. When you enter the MUD you start off in the MainInteractionMode, which presents the usual flow of play and all the usual commands. But the player can enter, say, a text editor mode for editing their description, which shows totally different information to the user and has totally different commands. Which mode should interpret an ordered command?

I made things simple. I look for the shallowest MainInteractionMode, which should always be there for any player or nipic (actually, nipics have a NipicInteractionMode, which is similar but doesn't allow OOC commands), and route the command to it. This clearly makes an assumption about the presence of this specific type of interaction mode, but I feel it's a pretty safe one. The code looks a bit like this.

mainIM = manager().findIntermode() { |im|
    im.instance_of?(MainInteractionMode)
}

DebugOutput.ASSERT(mainIM, "could not find a MainInteractionMode. order shouldn't be allowed at this point. ims: #{@intermodeStack}")

if !mainIM.processRemote(evento.order)
    result = EventoStatus::OrderEvento_F_BadCommand
end

Note that here we call processRemote, not process, which is what is normally called to translate each line of text input from the client and queue it up. This allows us to differentiate between local and remote commands. More on that later.

Restricting what can be ordered

It doesn't make sense to order a nipic to "ooc" something. That is a method of communication reserved for entities that have both an in-character aspect and an out-of-character aspect. Likewise, it doesn't make sense to allow a nipic to quit. In fact, it doesn't make sense to be able to order another player to do either of those things either, since ordering is an in-character concept. This piece of code in processRemote makes it so by checking the kind of invocation it is.

if !invocation.cmd.kind_of?(NullCommand) && invocation.kind_of?(ICInvocation)
    addInvocation(invocation, false)
    return true
end

Command delays

When you type a command in, you incur a delay that is specific to what command you typed. Most of the commands I've implemented so far have a 1-pulse delay, meaning they seem to execute pretty much immediately. In most MUDs the "flee" command has a several second delay, so that it's not too easy to get away from a fight. Right now in my MUD, the toy command "jump" has a 3 or so second delay, purely so that I can test out delays.

When someone else orders your character to do something, your character should delay for as long as the command takes to complete. If both you and another player are sending commands to your character, they should be interleaved in the order in which they were received.

Nicely, the interaction mode's queueing of commands and handling of command delays takes care of this for remote commands too, by inserting both local and remote commands into the same queue, servicing them in order, and applying the appropriate delay after each one.

Haxx to escape

I discovered two ways of circumventing orders that involve exploiting the fact that they are managed by the interaction mode. For the first way, I found this innocuous looking bit of code.

def onEnterDeeper()
    super()
    flushQueue()
    showPrompt()
end # function on enter deeper

Actually, I guess that doesn't really look that innocuous, does it? This function is called when the user types a command that causes a new interaction mode to take the foreground. So if the player started editing his description, which brings the TextDocumentEditorMode to the foreground, it would flush all the commands from the queue. The fix here is simply to remove this flushQueue call; it turns out some other changes I made render it unnecessary.

The other way I found is the FlushCommand, by which a user can type ~ and clear all the commands that he's typed but which have not yet executed. It's useful when you just typed a bunch of commands but made a mistake somewhere in there (like typing "jump" 10 times in a row, which would normally take 30 seconds to finish). Obviously, "order" would be kind of worthless if it were so easy to break free of it. The way I fixed this is by tagging command invocations in process and processRemote as local and remote, respectively. Then ~ can flush only the local commands.

Digression: Ruby-specific features

By the way, in implementing this last part, I broke my internal guideline not to use Ruby-specific language features.

module MIM_InvocationTag
    attr_reader :mimit_isLocal
    attr_writer :mimit_isLocal
end # module MIM_InvocationTag

def addInvocation(invocation, isLocal)
    invocation.extend(MIM_InvocationTag)
    invocation.mimit_isLocal = isLocal
    @invoQueue.addInvocation(invocation)
end # function addInvocation

def flushLocalInvocations(upToInvocation = nil)
    @invoQueue.flush(upToInvocation) { |invo|
        invo.mimit_isLocal
    }
end # function flushLocalInvocations

Ruby has the ability to modify the class of an object instance on the fly. I didn't want to add the local/remote attribute to the Invocation class, and it wasn't convenient to subclass it either. Adding a "tag" like this seems like a really useful thing to do, since it limits the code that "knows about" it. I try not to do this kind of thing too much because deep down I entertain the possibility that I could implement this MUD in another language. If I make use of too many Ruby-specific features, that possibility withers away.

Finally

Finally, as a reward for reading this far, I leave you with what I think is a neat example of how this works.

<type commands for a list> o man o jointface o man say confusing!
You bend a man to your will

<type commands for a list>
a man commands you to do his bidding.
You bend a man to your will

<type commands for a list>
a man says, 'confusing!'

0 comments:

Post a Comment