9. Parsing Command input

In this lesson we learn some basics about parsing the input of Commands. We will also learn how to add, modify and extend Evennia’s default commands.

9.1. More advanced parsing

In the last lesson we made a hit Command and struck a dragon with it. You should have the code from that still around.

Let’s expand our simple hit command to accept a little more complex input:

hit <target> [[with] <weapon>]

That is, we want to support all of these forms

hit target
hit target weapon
hit target with weapon

If you don’t specify a weapon you’ll use your fists. It’s also nice to be able to skip “with” if you are in a hurry. Time to modify mygame/commands/mycommands.py again. Let us break out the parsing a little, in a new method parse:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#...

class CmdHit(Command):
    """
    Hit a target.

    Usage:
      hit <target>

    """
    key = "hit"

    def parse(self):
        self.args = self.args.strip()
        target, *weapon = self.args.split(" with ", 1)
        if not weapon:
            target, *weapon = target.split(" ", 1)
        self.target = target.strip()
        if weapon:
            self.weapon = weapon[0].strip()
        else:
            self.weapon = ""

    def func(self):
        if not self.args:
            self.caller.msg("Who do you want to hit?")
            return
        # get the target for the hit
        target = self.caller.search(self.target)
        if not target:
            return
        # get and handle the weapon
        weapon = None
        if self.weapon:
            weapon = self.caller.search(self.weapon)
        if weapon:
            weaponstr = f"{weapon.key}"
        else:
            weaponstr = "bare fists"

        self.caller.msg(f"You hit {target.key} with {weaponstr}!")
        target.msg(f"You got hit by {self.caller.key} with {weaponstr}!")
# ...

The parse method is a special one Evennia knows to call before func. At this time it has access to all the same on-command variables as func does. Using parse not only makes things a little easier to read, it also means you can easily let other Commands inherit your parsing - if you wanted some other Command to also understand input on the form <arg> with <arg> you’d inherit from this class and just implement the func needed for that command without implementing parse anew.

  • Line 14 - We do the stripping of self.args once and for all here. We also store the stripped version back into self.args, overwriting it. So there is no way to get back the non-stripped version from here on, which is fine for this command.

  • Line 15 - This makes use of the .split method of strings. .split will, well, split the string by some criterion. .split(" with ", 1) means “split the string once, around the substring " with " if it exists”. The result of this split is a list. Just how that list looks depends on the string we are trying to split:

    1. If we entered just hit smaug, we’d be splitting just "smaug" which would give the result ["smaug"].

    2. hit smaug sword gives ["smaug sword"]

    3. hit smaug with sword gives ["smaug", "sword"]

    So we get a list of 1 or 2 elements. We assign it to two variables like this, target, *weapon = . That asterisk in *weapon is a nifty trick - it will automatically become a tuple of 0 or more values. It sorts of “soaks” up everything left over.

    1. target becomes "smaug" and weapon becomes () (an empty tuple)

    2. target becomes "smaug sword" and weapon becomes ()

    3. target becomes "smaug" and weapon becomes ("sword",) (this is a tuple with one element, the comma is required to indicate this).

  • Lines 16-17 - In this if condition we check if weapon is falsy (that is, the empty list). This can happen under two conditions (from the example above):

    1. target is simply smaug

    2. target is smaug sword

    To separate these cases we split target once again, this time by empty space " ". Again we store the result back with target, *weapon =. The result will be one of the following:

    1. target remains "smaug" and weapon remains []

    2. target becomes "smaug" and weapon becomes ("sword",)

  • Lines 18-22 - We now store target and weapon into self.target and self.weapon. We must store on self in order for these local variables to become available in func later. Note that once we know that weapon exists, it must be a tuple (like ("sword",)), so we use weapon[0] to get the first element of that tuple (tuples and lists in Python are indexed from 0). The instruction weapon[0].strip() can be read as “get the first string stored in the tuple weapon and remove all extra whitespace on it with .strip()”. If we forgot the [0] here, we’d get an error since a tuple (unlike the string inside the tuple) does not have the .strip() method.

Now onto the func method. The main difference is we now have self.target and self.weapon available for convenient use.

  • Lines 29 and 35 - We make use of the previously parsed search terms for the target and weapon to find the respective resource.

  • Lines 34-39 - Since the weapon is optional, we need to supply a default (use our fists!) if it’s not set. We use this to create a weaponstr that is different depending on if we have a weapon or not.

  • Lines 41-42 - We merge the weaponstr with our attack texts and send it to attacker and target respectively.

Let’s try it out!

> reload
> hit smaug with sword
Could not find 'sword'.
You hit smaug with bare fists!

Oops, our self.caller.search(self.weapon) is telling us that it found no sword. This is reasonable (we don’t have a sword). Since we are not returning when failing to find a weapon in the way we do if we find no target, we still continue fighting with our bare hands.

This won’t do. Let’s make ourselves a sword:

> create sword

Since we didn’t specify /drop, the sword will end up in our inventory and can seen with the i or inventory command. The .search helper will still find it there. There is no need to reload to see this change (no code changed, only stuff in the database).

> hit smaug with sword
You hit smaug with sword!

Poor Smaug.

9.2. Adding a Command to an object

As we learned in the lesson about Adding commands, Commands are are grouped in Command Sets. Such Command Sets are attached to an object with obj.cmdset.add() and will then be available for that object to use.

What we didn’t mention before is that by default those commands are also available to those in the same location as that object. If you did the Building quickstart lesson you’ve seen an example of this with the “Red Button” object. The Tutorial world also has many examples of objects with commands on them.

To show how this could work, let’s put our ‘hit’ Command on our simple sword object from the previous section.

> py self.search("sword").cmdset.add("commands.mycommands.MyCmdSet", persistent=True)

We find the sword (it’s still in our inventory so self.search should be able to find it), then add MyCmdSet to it. This actually adds both hit and echo to the sword, which is fine.

Let’s try to swing it!

> hit
More than one match for 'hit' (please narrow target):
hit-1 (sword #11)
hit-2

Woah, that didn’t go as planned. Evennia actually found two hit commands and didn’t know which one to use (we know they are the same, but Evennia can’t be sure of that). As we can see, hit-1 is the one found on the sword. The other one is from adding MyCmdSet to ourself earlier. It’s easy enough to tell Evennia which one you meant:

> hit-1
Who do you want to hit?
> hit-2
Who do you want to hit?

In this case we don’t need both command-sets, we should drop the version of hit sitting on our ourselves.

Go to mygame/commands/default_cmdsets.py and find the line where you added MyCmdSet in the previous lesson. Delete or comment it out:

# mygame/commands/default_cmdsets.py 

# ...

class CharacterCmdSet(default_cmds.CharacterCmdSet):

    # ... 
    def at_object_creation(self): 

        # self.add(MyCmdSet)    # <---------

Next reload and you’ll only have one hit command available:

> hit
Who do you want to hit?

Now try making a new location and then drop the sword in it.

> tunnel n = kitchen
> n
> drop sword
> s
> hit
Command 'hit' is not available. Maybe you meant ...
> n
> hit
Who do you want to hit?

The hit command is only available if you hold or are in the same room as the sword.

9.2.1. You need to hold the sword!

Let’s get a little ahead of ourselves and make it so you have to hold the sword for the hit command to be available. This involves a Lock. We’ll cover locks in more detail later, just know that they are useful for limiting the kind of things you can do with an object, including limiting just when you can call commands on it.

> py self.search("sword").locks.add("call:holds()")

We added a new lock to the sword. The lockstring "call:holds()" means that you can only call commands on this object if you are holding the object (that is, it’s in your inventory).

For locks to work, you cannot be superuser, since the superuser passes all locks. You need to quell yourself first:

> quell

If the sword lies on the ground, try

> hit
Command 'hit' is not available. ..
> get sword
> hit
> Who do you want to hit?

After we’ve waved the sword around (hit a dragon or two), we will get rid of ours sword so we have a clean slate with no more hit commands floating around. We can do that in two ways:

delete sword

or

py self.search("sword").delete()

9.3. Adding the Command to a default Cmdset

As we have seen we can use obj.cmdset.add() to add a new cmdset to objects, whether that object is ourself (self) or other objects like the sword. Doing this this way is a little cumbersome though. It would be better to add this to all characters.

The default cmdset are defined in mygame/commands/default_cmdsets.py. Open that file now:

"""
(module docstring)
"""

from evennia import default_cmds

class CharacterCmdSet(default_cmds.CharacterCmdSet):

    key = "DefaultCharacter"

    def at_cmdset_creation(self):

        super().at_cmdset_creation()
        #
        # any commands you add below will overload the default ones
        #

class AccountCmdSet(default_cmds.AccountCmdSet):

    key = "DefaultAccount"

    def at_cmdset_creation(self):

        super().at_cmdset_creation()
        #
        # any commands you add below will overload the default ones
        #

class UnloggedinCmdSet(default_cmds.UnloggedinCmdSet):

    key = "DefaultUnloggedin"

    def at_cmdset_creation(self):

        super().at_cmdset_creation()
        #
        # any commands you add below will overload the default ones
        #

class SessionCmdSet(default_cmds.SessionCmdSet):

    key = "DefaultSession"

    def at_cmdset_creation(self):

        super().at_cmdset_creation()
        #
        # any commands you add below will overload the default ones
        #

evennia.default_cmds is a container that holds all of Evennia’s default commands and cmdsets. In this module we can see that this was imported and then a new child class was made for each cmdset. Each class looks familiar (except the key, that’s mainly used to easily identify the cmdset in listings). In each at_cmdset_creation all we do is call super().at_cmdset_creation which means that we call `at_cmdset_creation() on the parent CmdSet.

This is what adds all the default commands to each CmdSet.

When the DefaultCharacter (or a child of it) is created, you’ll find that the equivalence of self.cmdset.add("default_cmdsets.CharacterCmdSet, persistent=True") gets called. This means that all new Characters get this cmdset. After adding more commands to it, you just need to reload to have all characters see it.

  • Characters (that is ‘you’ in the gameworld) has the CharacterCmdSet.

  • Accounts (the thing that represents your out-of-character existence on the server) has the AccountCmdSet

  • Sessions (representing one single client connection) has the SessionCmdSet

  • Before you log in (at the connection screen) your Session have access to the UnloggedinCmdSet.

For now, let’s add our own hit and echo commands to the CharacterCmdSet:

# ...

from commands import mycommands

class CharacterCmdSet(default_cmds.CharacterCmdSet):

    key = "DefaultCharacter"

    def at_cmdset_creation(self):

        super().at_cmdset_creation()
        #
        # any commands you add below will overload the default ones
        #
        self.add(mycommands.CmdEcho)
        self.add(mycommands.CmdHit)

> reload
> hit
Who do you want to hit?

Your new commands are now available for all player characters in the game. There is another way to add a bunch of commands at once, and that is to add your own CmdSet to the other cmdset.

from commands import mycommands

class CharacterCmdSet(default_cmds.CharacterCmdSet):

    key = "DefaultCharacter"

    def at_cmdset_creation(self):

        super().at_cmdset_creation()
        #
        # any commands you add below will overload the default ones
        #
        self.add(mycommands.MyCmdSet)

Which way you use depends on how much control you want, but if you already have a CmdSet, this is practical. A Command can be a part of any number of different CmdSets.

9.3.1. Removing Commands

To remove your custom commands again, you of course just delete the change you did to mygame/commands/default_cmdsets.py. But what if you want to remove a default command?

We already know that we use cmdset.remove() to remove a cmdset. It turns out you can do the same in at_cmdset_creation. For example, let’s remove the default get Command from Evennia. If you investigate the default_cmds.CharacterCmdSet parent, you’ll find that its class is default_cmds.CmdGet (the ‘real’ location is evennia.commands.default.general.CmdGet).

# ...
from commands import mycommands

class CharacterCmdSet(default_cmds.CharacterCmdSet):

    key = "DefaultCharacter"

    def at_cmdset_creation(self):

        super().at_cmdset_creation()
        #
        # any commands you add below will overload the default ones
        #
        self.add(mycommands.MyCmdSet)
        self.remove(default_cmds.CmdGet)
# ...
> reload
> get
Command 'get' is not available ...

9.4. Replace a default command

At this point you already have all the pieces for how to do this! We just need to add a new command with the same key in the CharacterCmdSet to replace the default one.

Let’s combine this with what we know about classes and how to override a parent class. Open mygame/commands/mycommands.py and make a new get command:

1
2
3
4
5
6
7
8
9
# up top, by the other imports
from evennia import default_cmds

# somewhere below
class MyCmdGet(default_cmds.CmdGet):

    def func(self):
        super().func()
        self.caller.msg(str(self.caller.location.contents))
  • Line 2: We import default_cmds so we can get the parent class. We made a new class and we make it inherit default_cmds.CmdGet. We don’t need to set .key or .parse, that’s already handled by the parent. In func we call super().func() to let the parent do its normal thing,

  • Line 7: By adding our own func we replace the one in the parent.

  • Line 8: For this simple change we still want the command to work the same as before, so we use super() to call func on the parent.

  • Line 9: .location is the place an object is at. .contents contains, well, the contents of an object. If you tried py self.contents you’d get a list that equals your inventory. For a room, the contents is everything in it. So self.caller.location.contents gets the contents of our current location. This is a list. In order send this to us with .msg we turn the list into a string. Python has a special function str() to do this.

We now just have to add this so it replaces the default get command. Open mygame/commands/default_cmdsets.py again:

# ...
from commands import mycommands

class CharacterCmdSet(default_cmds.CharacterCmdSet):

    key = "DefaultCharacter"

    def at_cmdset_creation(self):

        super().at_cmdset_creation()
        #
        # any commands you add below will overload the default ones
        #
        self.add(mycommands.MyCmdSet)
        self.add(mycommands.MyCmdGet)
# ...

We don’t need to use self.remove() first; just adding a command with the same key (get) will replace the default get we had from before.

> reload
> get
Get What?
[smaug, fluffy, YourName, ...]

We just made a new get-command that tells us everything we could pick up (well, we can’t pick up ourselves, so there’s some room for improvement there …).

9.5. Summary

In this lesson we got into some more advanced string formatting - many of those tricks will help you a lot in the future! We also made a functional sword. Finally we got into how to add to, extend and replace a default command on ourselves. Knowing to add commands is a big part of making a game!

We have been beating on poor Smaug for too long. Next we’ll create more things to play around with.