Complete permanent->persistent rename of cmdset kwarg for consistency

This commit is contained in:
Griatch 2021-08-06 17:16:44 +02:00
parent 6e38d0ae4c
commit a815db4ca9
20 changed files with 362 additions and 362 deletions

View file

@ -1,30 +1,30 @@
# Our own commands
In this lesson we'll learn how to create our own Evennia _Commands_. If you are new to Python you'll
also learn some more basics about how to manipulate strings and get information out of Evennia.
In this lesson we'll learn how to create our own Evennia _Commands_. If you are new to Python you'll
also learn some more basics about how to manipulate strings and get information out of Evennia.
A Command is something that handles the input from a user and causes a result to happen.
An example is `look`, which examines your current location and tells how it looks like and
what is in it.
what is in it.
```sidebar:: Commands are not typeclassed
If you just came from the previous lesson, you might want to know that Commands and
CommandSets are not `typeclassed`. That is, instances of them are not saved to the
If you just came from the previous lesson, you might want to know that Commands and
CommandSets are not `typeclassed`. That is, instances of them are not saved to the
database. They are "just" normal Python classes.
```
In Evennia, a Command is a Python _class_. If you are unsure about what a class is, review the
In Evennia, a Command is a Python _class_. If you are unsure about what a class is, review the
previous lessons! A Command inherits from `evennia.Command` or from one of the alternative command-
classes, such as `MuxCommand` which is what most default commands use.
classes, such as `MuxCommand` which is what most default commands use.
All Commands are in turn grouped in another class called a _Command Set_. Think of a Command Set
as a bag holding many different commands. One CmdSet could for example hold all commands for
combat, another for building etc. By default, Evennia groups all character-commands into one
big cmdset.
as a bag holding many different commands. One CmdSet could for example hold all commands for
combat, another for building etc. By default, Evennia groups all character-commands into one
big cmdset.
Command-Sets are then associated with objects, for example with your Character. Doing so makes the
commands in that cmdset available to the object. So, to summarize:
Command-Sets are then associated with objects, for example with your Character. Doing so makes the
commands in that cmdset available to the object. So, to summarize:
- Commands are classes
- A group of Commands is stored in a CmdSet
@ -45,25 +45,25 @@ from evennia import Command as BaseCommand
class Command(BaseCommand):
"""
(class docstring)
"""
"""
pass
# (lots of commented-out stuff)
# ...
```
Ignoring the docstrings (which you can read if you want), this is the only really active code in the module.
We can see that we import `Command` from `evennia` and use the `from ... import ... as ...` form to rename it
to `BaseCommand`. This is so we can let our child class also be named `Command` for reference. The class
itself doesn't do anything, it just has `pass`. So in the same way as `Object` in the previous lesson, this
class is identical to its parent.
itself doesn't do anything, it just has `pass`. So in the same way as `Object` in the previous lesson, this
class is identical to its parent.
> The commented out `default_cmds` gives us access to Evennia's default commands for easy overriding. We'll try
> that a little later.
> that a little later.
We could modify this module directly, but to train imports we'll work in a separate module. Open a new file
`mygame/commands/mycommands.py` and add the following code:
`mygame/commands/mycommands.py` and add the following code:
```python
@ -74,8 +74,8 @@ class CmdEcho(Command):
```
This is the simplest form of command you can imagine. It just gives itself a name, "echo". This is
what you will use to call this command later.
This is the simplest form of command you can imagine. It just gives itself a name, "echo". This is
what you will use to call this command later.
Next we need to put this in a CmdSet. It will be a one-command CmdSet for now! Change your file as such:
@ -96,24 +96,24 @@ class MyCmdSet(CmdSet):
```
Our `EchoCmdSet` class must have an `at_cmdset_creation` method, named exactly
like this - this is what Evennia will be looking for when setting up the cmdset later, so
if you didn't set it up, it will use the parent's version, which is empty. Inside we add the
command class to the cmdset by `self.add()`. If you wanted to add more commands to this CmdSet you
could just add more lines of `self.add` after this.
Our `EchoCmdSet` class must have an `at_cmdset_creation` method, named exactly
like this - this is what Evennia will be looking for when setting up the cmdset later, so
if you didn't set it up, it will use the parent's version, which is empty. Inside we add the
command class to the cmdset by `self.add()`. If you wanted to add more commands to this CmdSet you
could just add more lines of `self.add` after this.
Finally, let's add this command to ourselves so we can try it out. In-game you can experiment with `py` again:
> py self.cmdset.add("commands.mycommands.MyCmdSet")
Now try
> echo
Now try
> echo
Command echo has no defined `func()` - showing on-command variables:
...
...
You should be getting a long list of outputs. The reason for this is that your `echo` function is not really
You should be getting a long list of outputs. The reason for this is that your `echo` function is not really
"doing" anything yet and the default function is then to show all useful resources available to you when you
use your Command. Let's look at some of those listed:
@ -124,7 +124,7 @@ use your Command. Let's look at some of those listed:
cmdname (<class 'str'>): echo
raw_cmdname (<class 'str'>): echo
cmdstring (<class 'str'>): echo
args (<class 'str'>):
args (<class 'str'>):
cmdset (<class 'evennia.commands.cmdset.CmdSet'>): @mail, about, access, accounts, addcom, alias, allcom, ban, batchcode, batchcommands, boot, cboot, ccreate,
cdesc, cdestroy, cemit, channels, charcreate, chardelete, checklockstring, clientwidth, clock, cmdbare, cmdsets, color, copy, cpattr, create, cwho, delcom,
desc, destroy, dig, dolphin, drop, echo, emit, examine, find, force, get, give, grapevine2chan, help, home, ic, inventory, irc2chan, ircstatus, link, lock,
@ -133,7 +133,7 @@ use your Command. Let's look at some of those listed:
tickers, time, tunnel, typeclass, unban, unlink, up, up, userpassword, wall, whisper, who, wipe
session (<class 'evennia.server.serversession.ServerSession'>): Griatch(#1)@1:2:7:.:0:.:0:.:1
account (<class 'typeclasses.accounts.Account'>): Griatch(account 1)
raw_string (<class 'str'>): echo
raw_string (<class 'str'>): echo
--------------------------------------------------
echo - Command variables from evennia:
@ -147,16 +147,16 @@ use your Command. Let's look at some of those listed:
command string given (self.cmdstring): echo
current cmdset (self.cmdset): ChannelCmdSet
These are all properties you can access with `.` on the Command instance, such as `.key`, `.args` and so on.
Evennia makes these available to you and they will be different every time a command is run. The most
important ones we will make use of now are:
These are all properties you can access with `.` on the Command instance, such as `.key`, `.args` and so on.
Evennia makes these available to you and they will be different every time a command is run. The most
important ones we will make use of now are:
- `caller` - this is 'you', the person calling the command.
- `args` - this is all arguments to the command. Now it's empty, but if you tried `echo foo bar` you'd find
- `args` - this is all arguments to the command. Now it's empty, but if you tried `echo foo bar` you'd find
that this would be `" foo bar"`.
- `obj` - this is object on which this Command (and CmdSet) "sits". So you, in this case.
The reason our command doesn't do anything yet is because it's missing a `func` method. This is what Evennia
The reason our command doesn't do anything yet is because it's missing a `func` method. This is what Evennia
looks for to figure out what a Command actually does. Modify your `CmdEcho` class:
```python
@ -165,47 +165,47 @@ looks for to figure out what a Command actually does. Modify your `CmdEcho` clas
class CmdEcho(Command):
"""
A simple echo command
Usage:
echo <something>
echo <something>
"""
key = "echo"
def func(self):
self.caller.msg(f"Echo: '{self.args}'")
self.caller.msg(f"Echo: '{self.args}'")
# ...
```
First we added a docstring. This is always a good thing to do in general, but for a Command class, it will also
automatically become the in-game help entry! Next we add the `func` method. It has one active line where it
makes use of some of those variables we found the Command offers to us. If you did the
First we added a docstring. This is always a good thing to do in general, but for a Command class, it will also
automatically become the in-game help entry! Next we add the `func` method. It has one active line where it
makes use of some of those variables we found the Command offers to us. If you did the
[basic Python tutorial](./Python-basic-introduction), you will recognize `.msg` - this will send a message
to the object it is attached to us - in this case `self.caller`, that is, us. We grab `self.args` and includes
that in the message.
that in the message.
Since we haven't changed `MyCmdSet`, that will work as before. Reload and re-add this command to ourselves to
try out the new version:
> reload
Since we haven't changed `MyCmdSet`, that will work as before. Reload and re-add this command to ourselves to
try out the new version:
> reload
> py self.cmdset.add("commands.mycommands.MyCmdSet")
> echo
> echo
Echo: ''
Try to pass an argument:
Try to pass an argument:
> echo Woo Tang!
Echo: ' Woo Tang!'
Note that there is an extra space before `Woo!`. That is because self.args contains the _everything_ after
the command name, including spaces. Evennia will happily understand if you skip that space too:
the command name, including spaces. Evennia will happily understand if you skip that space too:
> echoWoo Tang!
Echo: 'Woo Tang!'
There are ways to force Evennia to _require_ an initial space, but right now we want to just ignore it since
it looks a bit weird for our echo example. Tweak the code:
There are ways to force Evennia to _require_ an initial space, but right now we want to just ignore it since
it looks a bit weird for our echo example. Tweak the code:
```python
# ...
@ -213,15 +213,15 @@ it looks a bit weird for our echo example. Tweak the code:
class CmdEcho(Command):
"""
A simple echo command
Usage:
echo <something>
echo <something>
"""
key = "echo"
def func(self):
self.caller.msg(f"Echo: '{self.args.strip()}'")
self.caller.msg(f"Echo: '{self.args.strip()}'")
# ...
```
@ -230,25 +230,25 @@ The only difference is that we called `.strip()` on `self.args`. This is a helpe
strings - it strips out all whitespace before and after the string. Now the Command-argument will no longer
have any space in front of it.
> reload
> reload
> py self.cmdset.add("commands.mycommands.MyCmdSet")
> echo Woo Tang!
Echo: 'Woo Tang!'
Don't forget to look at the help for the echo command:
> help echo
You will get the docstring you put in your Command-class.
Don't forget to look at the help for the echo command:
> help echo
You will get the docstring you put in your Command-class.
### Making our cmdset persistent
It's getting a little annoying to have to re-add our cmdset every time we reload, right? It's simple
enough to make `echo` a _permanent_ change though:
enough to make `echo` a _persistent_ change though:
> py self.cmdset.add("commands.mycommands.MyCmdSet", permanent=True)
Now you can `reload` as much as you want and your code changes will be available directly without
> py self.cmdset.add("commands.mycommands.MyCmdSet", persistent=True)
Now you can `reload` as much as you want and your code changes will be available directly without
needing to re-add the MyCmdSet again. To remove the cmdset again, do
> py self.cmdset.remove("commands.mycommands.MyCmdSet")
@ -258,18 +258,18 @@ But for now, keep it around, we'll expand it with some more examples.
### Figuring out who to hit
Let's try something a little more exciting than just echo. Let's make a `hit` command, for punching
someone in the face! This is how we want it to work:
someone in the face! This is how we want it to work:
> hit <target>
You hit <target> with full force!
You hit <target> with full force!
Not only that, we want the <target> to see
You got hit by <hitter> with full force!
You got hit by <hitter> with full force!
Here, `<hitter>` would be the one using the `hit` command and `<target>` is the one doing the punching.
Here, `<hitter>` would be the one using the `hit` command and `<target>` is the one doing the punching.
Still in `mygame/commands/mycommands.py`, add a new class, between `CmdEcho` and `MyCmdSet`.
Still in `mygame/commands/mycommands.py`, add a new class, between `CmdEcho` and `MyCmdSet`.
```python
# ...
@ -277,7 +277,7 @@ Still in `mygame/commands/mycommands.py`, add a new class, between `CmdEcho` and
class CmdHit(Command):
"""
Hit a target.
Usage:
hit <target>
@ -288,11 +288,11 @@ class CmdHit(Command):
args = self.args.strip()
if not args:
self.caller.msg("Who do you want to hit?")
return
return
target = self.caller.search(args)
if not target:
return
self.caller.msg(f"You hit {target.key} with full force!")
return
self.caller.msg(f"You hit {target.key} with full force!")
target.msg(f"You got hit by {self.caller.key} with full force!")
# ...
@ -302,46 +302,46 @@ A lot of things to dissect here:
- **Line 4**: The normal `class` header. We inherit from `Command` which we imported at the top of this file.
- **Lines 5**-11: The docstring and help-entry for the command. You could expand on this as much as you wanted.
- **Line 12**: We want to write `hit` to use this command.
- **Line 15**: We strip the whitespace from the argument like before. Since we don't want to have to do
- **Line 15**: We strip the whitespace from the argument like before. Since we don't want to have to do
`self.args.strip()` over and over, we store the stripped version
in a _local variable_ `args`. Note that we don't modify `self.args` by doing this, `self.args` will still
have the whitespace and is not the same as `args` in this example.
```sidebar:: if-statements
The full form of the if statement is
The full form of the if statement is
if condition:
...
elif othercondition:
...
else:
else:
...
There can be any number of `elifs` to mark when different branches of the code should run. If
the `else` condition is given, it will run if none of the other conditions was truthy. In Python
the `if..elif..else` structure also serves the same function as `case` in some other languages.
There can be any number of `elifs` to mark when different branches of the code should run. If
the `else` condition is given, it will run if none of the other conditions was truthy. In Python
the `if..elif..else` structure also serves the same function as `case` in some other languages.
```
- **Line 16** has our first _conditional_, an `if` statement. This is written on the form `if <condition>:` and only
if that condition is 'truthy' will the indented code block under the `if` statement run. To learn what is truthy in
Python it's usually easier to learn what is "falsy":
Python it's usually easier to learn what is "falsy":
- `False` - this is a reserved boolean word in Python. The opposite is `True`.
- `None` - another reserved word. This represents nothing, a null-result or value.
- `0` or `0.0`
- `0` or `0.0`
- The empty string `""` or `''` or `""""""` or `''''''`
- Empty _iterables_ we haven't seen yet, like empty lists `[]`, empty tuples `()` and empty dicts `{}`.
- Everything else is "truthy".
- Everything else is "truthy".
Line 16's condition is `not args`. The `not` _inverses_ the result, so if `args` is the empty string (falsy), the
whole conditional becomes truthy. Let's continue in the code:
whole conditional becomes truthy. Let's continue in the code:
- **Lines 17-18**: This code will only run if the `if` statement is truthy, in this case if `args` is the empty string.
- **Line 18**: `return` is a reserved Python word that exits `func` immediately.
- **Line 18**: `return` is a reserved Python word that exits `func` immediately.
- **Line 19**: We use `self.caller.search` to look for the target in the current location.
- **Lines 20-21**: A feature of `.search` is that it will already inform `self.caller` if it couldn't find the target.
In that case, `target` will be `None` and we should just directly `return`.
In that case, `target` will be `None` and we should just directly `return`.
- **Lines 22-23**: At this point we have a suitable target and can send our punching strings to each.
Finally we must also add this to a CmdSet. Let's add it to `MyCmdSet` which we made permanent earlier.
Finally we must also add this to a CmdSet. Let's add it to `MyCmdSet` which we made persistent earlier.
```python
# ...
@ -357,17 +357,17 @@ class MyCmdSet(CmdSet):
```sidebar:: Errors in your code
With longer code snippets to try, it gets more and more likely you'll
make an error and get a `traceback` when you reload. This will either appear
directly in-game or in your log (view it with `evennia -l` in a terminal).
Don't panic; tracebacks are your friends - they are to be read bottom-up and usually describe
make an error and get a `traceback` when you reload. This will either appear
directly in-game or in your log (view it with `evennia -l` in a terminal).
Don't panic; tracebacks are your friends - they are to be read bottom-up and usually describe
exactly where your problem is. Refer to `The Python intro <Python-basic-introduction.html>`_ for
more hints. If you get stuck, reach out to the Evennia community for help.
```
Next we reload to let Evennia know of these code changes and try it out:
Next we reload to let Evennia know of these code changes and try it out:
> reload
> reload
hit
Who do you want to hit?
hit me
@ -377,7 +377,7 @@ Next we reload to let Evennia know of these code changes and try it out:
Lacking a target, we hit ourselves. If you have one of the dragons still around from the previous lesson
you could try to hit it (if you dare):
hit smaug
hit smaug
You hit Smaug with full force!
You won't see the second string. Only Smaug sees that (and is not amused).
@ -385,8 +385,8 @@ You won't see the second string. Only Smaug sees that (and is not amused).
## Summary
In this lesson we learned how to create our own Command, add it to a CmdSet and then to ourselves.
We also upset a dragon.
In this lesson we learned how to create our own Command, add it to a CmdSet and then to ourselves.
We also upset a dragon.
In the next lesson we'll learn how to hit Smaug with different weapons. We'll also
get into how we replace and extend Evennia's default Commands.
In the next lesson we'll learn how to hit Smaug with different weapons. We'll also
get into how we replace and extend Evennia's default Commands.

View file

@ -1,25 +1,25 @@
# More about Commands
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.
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.
## More advanced parsing
## 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.
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:
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
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
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`:
@ -29,19 +29,19 @@ a little, in a new method `parse`:
class CmdHit(Command):
"""
Hit a target.
Usage:
hit <target>
"""
key = "hit"
def parse(self):
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()
target, *weapon = target.split(" ", 1)
self.target = target.strip()
if weapon:
self.weapon = weapon.strip()
else:
@ -50,162 +50,162 @@ class CmdHit(Command):
def func(self):
if not self.args:
self.caller.msg("Who do you want to hit?")
return
return
# get the target for the hit
target = self.caller.search(self.target)
target = self.caller.search(self.target)
if not target:
return
# get and handle the weapon
return
# get and handle the weapon
weapon = None
if self.weapon:
weapon = self.caller.search(self.weapon)
if weapon:
if weapon:
weaponstr = f"{weapon.key}"
else:
weaponstr = "bare fists"
self.caller.msg(f"You hit {target.key} with {weaponstr}!")
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_
`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.
```sidebar:: Tuples and Lists
```sidebar:: Tuples and Lists
- A `list` is written as `[a, b, c, d, ...]`. You can add and grow/shrink a list after it was first created.
- A `tuple` is written as `(a, b, c, d, ...)`. A tuple cannot be modified once it is created.
- A `list` is written as `[a, b, c, d, ...]`. You can add and grow/shrink a list after it was first created.
- A `tuple` is written as `(a, b, c, d, ...)`. A tuple cannot be modified once it is created.
```
- **Line 14** - We do the stripping of `self.args` once and for all here. We also store the stripped version back
- **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.
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
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.
1. `target` becomes `"smaug"` and `weapon` becomes `[]`
2. `target` becomes `"smaug sword"` and `weapon` becomes `[]`
3. `target` becomes `"smaug"` and `weapon` becomes `sword`
- **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):
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
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 do this in order
for these local variables to made available in `func` later. Note how we need to check so `weapon` is not falsy
before running `strip()` on it. This is because we know that if it's falsy, it's an empty list `[]` and lists
before running `strip()` 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
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
> 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
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!
> hit smaug with sword
You hit smaug with sword!
## Adding a Command to an object
## 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](./Building-Quickstart)
you've seen an example of this with the "Red Button" object. The [Tutorial world](./Tutorial-World-Introduction)
also has many examples of objects with commands on them.
you've seen an example of this with the "Red Button" object. The [Tutorial world](./Tutorial-World-Introduction)
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", permanent=True)
> 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.
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
> hit
More than one match for 'hit' (please narrow target):
hit-1 (sword #11)
hit-2
```sidebar:: Multi-matches
Some game engines will just pick the first hit when finding more than one.
Evennia will always give you a choice. The reason for this is that Evennia
cannot know if `hit` and `hit` are different or the same - maybe it behaves
differently depending on the object it sits on? Besides, imagine if you had
differently depending on the object it sits on? Besides, imagine if you had
a red and a blue button both with the command `push` on it. Now you just write
`push`. Wouldn't you prefer to be asked `which` button you really wanted to push?
```
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:
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
> 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:
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:
Now try this:
> tunnel n = kitchen
> n
> drop sword
> 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.
> 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.
### 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
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.
it.
```sidebar:: Locks
Evennia Locks are defined as a mini-language defined in `lockstrings`. The lockstring
@ -215,49 +215,49 @@ 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).
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:
first:
```sidebar:: quell/unquell
Quelling allows you as a developer to take on the role of players with less
priveleges. This is useful for testing and debugging, in particular since a
priveleges. This is useful for testing and debugging, in particular since a
superuser has a little `too` much power sometimes.
Use `unquell` to get back to your normal self.
```
> quell
If the sword lies on the ground, try
> hit
Command 'hit' is not available. ..
> get sword
> hit
> 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
delete sword
py self.search("sword").delete()
or
py self.search("sword").delete()
## 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`.
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.
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`
@ -266,7 +266,7 @@ All these commands are in just loaded on the default objects that Evennia provid
The thing must commonly modified is the `CharacterCmdSet`.
The default cmdset are defined in `mygame/commands/default_cmdsets.py`. Open that file now:
The default cmdset are defined in `mygame/commands/default_cmdsets.py`. Open that file now:
```python
"""
@ -321,19 +321,19 @@ class SessionCmdSet(default_cmds.SessionCmdSet):
```
```sidebar:: super()
The `super()` function refers to the parent of the current class and is commonly
used to call same-named methods on the parent.
used to call same-named methods on the parent.
```
`evennia.default_cmds` is a container that holds all of Evennia's default commands and cmdsets. In this module
`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.
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.
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`:
@ -358,9 +358,9 @@ class CharacterCmdSet(default_cmds.CharacterCmdSet):
```
> reload
> reload
> hit
Who do you want to 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:
@ -381,19 +381,19 @@ class CharacterCmdSet(default_cmds.CharacterCmdSet):
self.add(mycommands.MyCmdSet)
```
Which way you use depends on how much control you want, but if you already have a CmdSet,
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.
### 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?
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
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`.
```python
# ...
from commands import mycommands
@ -413,16 +413,16 @@ class CharacterCmdSet(default_cmds.CharacterCmdSet):
# ...
```
> reload
> reload
> get
Command 'get' is not available ...
## 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.
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
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.
@ -430,7 +430,7 @@ that `CmdGet` command.
# up top, by the other imports
from evennia import default_cmds
# somewhere below
# somewhere below
class MyCmdGet(default_cmds.CmdGet):
def func(self):
@ -440,20 +440,20 @@ class MyCmdGet(default_cmds.CmdGet):
```
- **Line2**: 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,
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
- **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.
- **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
We now just have to add this so it replaces the default `get` command. Open
`mygame/commands/default_cmdsets.py` again:
```python
@ -476,22 +476,22 @@ class CharacterCmdSet(default_cmds.CharacterCmdSet):
```
```sidebar:: Another way
Instead of adding `MyCmdGet` explicitly in default_cmdset.py,
you could also add it to `mycommands.MyCmdSet` and let it be
Instead of adding `MyCmdGet` explicitly in default_cmdset.py,
you could also add it to `mycommands.MyCmdSet` and let it be
added automatically for you.
```
> reload
> get
> reload
> get
Get What?
[smaug, fluffy, YourName, ...]
[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).
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).
## 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
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.

View file

@ -56,8 +56,8 @@ class Character(DefaultCharacter):
[...]
"""
def at_object_creation(self):
"This is called when object is first created, only."
self.db.power = 1
"This is called when object is first created, only."
self.db.power = 1
self.db.combat_score = 1
```
@ -94,10 +94,10 @@ check it. Using this method however will make it easy to add more functionality
What we need are the following:
- One character generation [Command](../../../Components/Commands) to set the "Power" on the `Character`.
- A chargen [CmdSet](../../../Components/Command-Sets) to hold this command. Lets call it `ChargenCmdset`.
- A custom `ChargenRoom` type that makes this set of commands available to players in such rooms.
- One such room to test things in.
- One character generation [Command](../../../Components/Commands) to set the "Power" on the `Character`.
- A chargen [CmdSet](../../../Components/Command-Sets) to hold this command. Lets call it `ChargenCmdset`.
- A custom `ChargenRoom` type that makes this set of commands available to players in such rooms.
- One such room to test things in.
### The +setpower command
@ -114,7 +114,7 @@ Open `command.py` file. It contains documented empty templates for the base comm
`MuxCommand` class offers some extra features like stripping whitespace that may be useful - if so,
just import from that instead.
Add the following to the end of the `command.py` file:
Add the following to the end of the `command.py` file:
```python
# end of command.py
@ -124,13 +124,13 @@ class CmdSetPower(Command):
"""
set the power of a character
Usage:
Usage:
+setpower <1-10>
This sets the power of the current character. This can only be
used during character generation.
This sets the power of the current character. This can only be
used during character generation.
"""
key = "+setpower"
help_category = "mush"
@ -138,10 +138,10 @@ class CmdSetPower(Command):
"This performs the actual command"
errmsg = "You must supply a number between 1 and 10."
if not self.args:
self.caller.msg(errmsg)
self.caller.msg(errmsg)
return
try:
power = int(self.args)
power = int(self.args)
except ValueError:
self.caller.msg(errmsg)
return
@ -180,7 +180,7 @@ class ChargenCmdset(CmdSet):
key = "Chargen"
def at_cmdset_creation(self):
"This is called at initialization"
self.add(command.CmdSetPower())
self.add(command.CmdSetPower())
```
In the future you can add any number of commands to this cmdset, to expand your character generation
@ -193,10 +193,10 @@ It's cleaner to put it on a room, so it's only available when players are in tha
We will create a simple Room typeclass to act as a template for all our Chargen areas. Edit
`mygame/typeclasses/rooms.py` next:
```python
```python
from commands.default_cmdsets import ChargenCmdset
# ...
# ...
# down at the end of rooms.py
class ChargenRoom(Room):
@ -206,10 +206,10 @@ class ChargenRoom(Room):
"""
def at_object_creation(self):
"this is called only at first creation"
self.cmdset.add(ChargenCmdset, permanent=True)
self.cmdset.add(ChargenCmdset, persistent=True)
```
Note how new rooms created with this typeclass will always start with `ChargenCmdset` on themselves.
Don't forget the `permanent=True` keyword or you will lose the cmdset after a server reload. For
Don't forget the `persistent=True` keyword or you will lose the cmdset after a server reload. For
more information about [Command Sets](../../../Components/Command-Sets) and [Commands](../../../Components/Commands), see the respective
links.
@ -235,7 +235,7 @@ as `chargen;character generation`.
So in summary, this will create a new room of type ChargenRoom and open an exit `chargen` to it and
an exit back here named `finish`. If you see errors at this stage, you must fix them in your code.
`@reload`
between fixes. Don't continue until the creation seems to have worked okay.
between fixes. Don't continue until the creation seems to have worked okay.
chargen
@ -263,19 +263,19 @@ set during Character generation:
> +attack
You +attack with a combat score of 12!
Go back to `mygame/commands/command.py` and add the command to the end like this:
Go back to `mygame/commands/command.py` and add the command to the end like this:
```python
```python
import random
# ...
# ...
class CmdAttack(Command):
"""
issues an attack
issues an attack
Usage:
+attack
Usage:
+attack
This will calculate a new combat score based on your Power.
Your combat score is visible to everyone in the same location.
@ -288,8 +288,8 @@ class CmdAttack(Command):
caller = self.caller
power = caller.db.power
if not power:
# this can happen if caller is not of
# our custom Character typeclass
# this can happen if caller is not of
# our custom Character typeclass
power = 1
combat_score = random.randint(1, 10 * power)
caller.db.combat_score = combat_score
@ -297,10 +297,10 @@ class CmdAttack(Command):
# announce
message = "%s +attack%s with a combat score of %s!"
caller.msg(message % ("You", "", combat_score))
caller.location.msg_contents(message %
caller.location.msg_contents(message %
(caller.key, "s", combat_score),
exclude=caller)
```
```
What we do here is simply to generate a "combat score" using Python's inbuilt `random.randint()`
function. We then store that and echo the result to everyone involved.
@ -349,8 +349,8 @@ class Character(DefaultCharacter):
[...]
"""
def at_object_creation(self):
"This is called when object is first created, only."
self.db.power = 1
"This is called when object is first created, only."
self.db.power = 1
self.db.combat_score = 1
def return_appearance(self, looker):
@ -395,16 +395,16 @@ instead put all relevant NPC commands in the default command set and limit event
### Creating an NPC with +createNPC
We need a command for creating the NPC, this is a very straightforward command:
We need a command for creating the NPC, this is a very straightforward command:
> +createnpc Anna
You created the NPC 'Anna'.
You created the NPC 'Anna'.
At the end of `command.py`, create our new command:
```python
from evennia import create_object
class CmdCreateNPC(Command):
"""
create a new npc
@ -413,12 +413,12 @@ class CmdCreateNPC(Command):
+createNPC <name>
Creates a new, named NPC. The NPC will start with a Power of 1.
"""
"""
key = "+createnpc"
aliases = ["+createNPC"]
locks = "call:not perm(nonpcs)"
help_category = "mush"
help_category = "mush"
def func(self):
"creates the object and names it"
caller = self.caller
@ -432,15 +432,15 @@ class CmdCreateNPC(Command):
# make name always start with capital letter
name = self.args.strip().capitalize()
# create npc in caller's location
npc = create_object("characters.Character",
key=name,
npc = create_object("characters.Character",
key=name,
location=caller.location,
locks="edit:id(%i) and perm(Builders);call:false()" % caller.id)
# announce
# announce
message = "%s created the NPC '%s'."
caller.msg(message % ("You", name))
caller.location.msg_contents(message % (caller.key, name),
exclude=caller)
caller.msg(message % ("You", name))
caller.location.msg_contents(message % (caller.key, name),
exclude=caller)
```
Here we define a `+createnpc` (`+createNPC` works too) that is callable by everyone *not* having the
`nonpcs` "[permission](../../../Components/Locks#Permissions)" (in Evennia, a "permission" can just as well be used to
@ -475,38 +475,38 @@ principle re-work our old `+setpower` command, but let's try something more usef
`+editNPC` command.
> +editNPC Anna/power = 10
Set Anna's property 'power' to 10.
Set Anna's property 'power' to 10.
This is a slightly more complex command. It goes at the end of your `command.py` file as before.
This is a slightly more complex command. It goes at the end of your `command.py` file as before.
```python
class CmdEditNPC(Command):
"""
edit an existing NPC
Usage:
Usage:
+editnpc <name>[/<attribute> [= value]]
Examples:
+editnpc mynpc/power = 5
+editnpc mynpc/power - displays power value
+editnpc mynpc - shows all editable
+editnpc mynpc - shows all editable
attributes and values
This command edits an existing NPC. You must have
This command edits an existing NPC. You must have
permission to edit the NPC to use this.
"""
key = "+editnpc"
aliases = ["+editNPC"]
locks = "cmd:not perm(nonpcs)"
help_category = "mush"
help_category = "mush"
def parse(self):
"We need to do some parsing here"
args = self.args
propname, propval = None, None
if "=" in args:
args, propval = [part.strip() for part in args.rsplit("=", 1)]
if "=" in args:
args, propval = [part.strip() for part in args.rsplit("=", 1)]
if "/" in args:
args, propname = [part.strip() for part in args.rsplit("/", 1)]
# store, so we can access it below in func()
@ -519,38 +519,38 @@ class CmdEditNPC(Command):
"do the editing"
allowed_propnames = ("power", "attribute1", "attribute2")
caller = self.caller
if not self.args or not self.name:
caller.msg("Usage: +editnpc name[/propname][=propval]")
caller.msg("Usage: +editnpc name[/propname][=propval]")
return
npc = caller.search(self.name)
if not npc:
return
if not npc.access(caller, "edit"):
caller.msg("You cannot change this NPC.")
return
return
if not self.propname:
# this means we just list the values
output = "Properties of %s:" % npc.key
for propname in allowed_propnames:
for propname in allowed_propnames:
propvalue = npc.attributes.get(propname, default="N/A")
output += "\n %s = %s" % (propname, propvalue)
caller.msg(output)
elif self.propname not in allowed_propnames:
caller.msg("You may only change %s." %
elif self.propname not in allowed_propnames:
caller.msg("You may only change %s." %
", ".join(allowed_propnames))
elif self.propval:
# assigning a new propvalue
# in this example, the properties are all integers...
intpropval = int(self.propval)
npc.attributes.add(self.propname, intpropval)
intpropval = int(self.propval)
npc.attributes.add(self.propname, intpropval)
caller.msg("Set %s's property '%s' to %s" %
(npc.key, self.propname, self.propval))
else:
# propname set, but not propval - show current value
caller.msg("%s has property %s = %s" %
(npc.key, self.propname,
caller.msg("%s has property %s = %s" %
(npc.key, self.propname,
npc.attributes.get(self.propname, default="N/A")))
```
@ -559,7 +559,7 @@ checking. It searches for the given npc in the same room, and checks so the call
permission to "edit" it before continuing. An account without the proper permission won't even be
able to view the properties on the given NPC. It's up to each game if this is the way it should be.
Add this to the default command set like before and you should be able to try it out.
Add this to the default command set like before and you should be able to try it out.
_Note: If you wanted a player to use this command to change an on-object property like the NPC's
name (the `key` property), you'd need to modify the command since "key" is not an Attribute (it is
@ -587,11 +587,11 @@ class CmdNPC(Command):
"""
controls an NPC
Usage:
Usage:
+npc <name> = <command>
This causes the npc to perform a command as itself. It will do so
with its own permissions and accesses.
with its own permissions and accesses.
"""
key = "+npc"
locks = "call:not perm(nonpcs)"
@ -601,7 +601,7 @@ class CmdNPC(Command):
"Simple split of the = sign"
name, cmdname = None, None
if "=" in self.args:
name, cmdname = [part.strip()
name, cmdname = [part.strip()
for part in self.args.rsplit("=", 1)]
self.name, self.cmdname = name, cmdname
@ -611,7 +611,7 @@ class CmdNPC(Command):
if not self.cmdname:
caller.msg("Usage: +npc <name> = <command>")
return
npc = caller.search(self.name)
npc = caller.search(self.name)
if not npc:
return
if not npc.access(caller, "edit"):
@ -651,4 +651,4 @@ specific player (or npc) and automatically compare their relevant attributes to
To continue from here, you can take a look at the [Tutorial World](../Part1/Tutorial-World-Introduction). For
more specific ideas, see the [other tutorials and hints](../../Howto-Overview) as well
as the [Evennia Component overview](../../../Components/Components-Overview).
as the [Evennia Component overview](../../../Components/Components-Overview).