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 hit 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
:
#...
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.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 called before func
and has access to all the same on-command variables as in func
. 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 intoself.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:If we entered just
hit smaug
, we’d be splitting just"smaug"
which would give the result["smaug"]
.hit smaug sword
gives["smaug sword"]
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 list of 0 or more values. It sorts of “soaks” up everything left over.target
becomes"smaug"
andweapon
becomes[]
target
becomes"smaug sword"
andweapon
becomes[]
target
becomes"smaug"
andweapon
becomessword
Lines 16-17 - In this
if
condition we check ifweapon
is falsy (that is, the empty list). This can happen under two conditions (from the example above):target
is simplysmaug
target
issmaug sword
To separate these cases we split
target
once again, this time by empty space" "
. Again we store the result back withtarget, *weapon =
. The result will be one of the following:target
remainssmaug
andweapon
remains[]
target
becomessmaug
andweapon
becomessword
Lines 18-22 - We now store
target
andweapon
intoself.target
andself.weapon
. We must do this in order for these local variables to made available infunc
later. Note how we need to check soweapon
is not falsy before runningstrip()
on it. This is because we know that if it’s falsy, it’s an empty list[]
and lists don’t have the.strip()
method on them (so if we tried to use it, we’d get an error).
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 text.
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. Since we are not return
ing
in this situation (like we do if failing to find 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!
9.2. Adding a Command to an object¶
The commands of a cmdset attached to an object with obj.cmdset.add()
will by default be made available to that object
but also to those in the same location as that object. If you did the Building introduction
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.
> 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 to 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, so let’s just keep the one on the sword:
> self.cmdset.remove("commands.mycommands.MyCmdSet")
> hit
Who do you want to hit?
Now try this:
> 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 now 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’ve 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?
Finally, we 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
.
This is how all commands in Evennia work, including default commands like look
, dig
, inventory
and so on.
All these commands are in just loaded on the default objects that Evennia provides out of the box.
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) you’ll have access to the
UnloggedinCmdSet
.
The thing must commonly modified is the CharacterCmdSet
.
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.
To add even more Commands to a default cmdset, we can just add them below the super()
line. Usefully, if we were to
add a Command with the same .key
as a default command, it would completely replace that original. So if you were
to add a command with a key look
, the original look
command would be replaced by your own version.
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 a CmdSet to the other cmdset. All commands in that cmdset will then be added:
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. We happen to know this can be found as default_cmds.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 lets override
that CmdGet
command.
# 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))
Line2: We import
default_cmds
so we can get the parent class. We made a new class and we make it inheritdefault_cmds.CmdGet
. We don’t need to set.key
or.parse
, that’s already handled by the parent. Infunc
we callsuper().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 callfunc
on the parent.Line 9:
.location
is the place an object is at..contents
contains, well, the contents of an object. If you triedpy self.contents
you’d get a list that equals your inventory. For a room, the contents is everything in it. Soself.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 functionstr()
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)
# ...
> 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.