Further updates

This commit is contained in:
Griatch 2020-07-03 22:32:50 +02:00
parent 830f793aa4
commit a515ececff
11 changed files with 856 additions and 540 deletions

View file

@ -1,6 +1,9 @@
# Adding Command Tutorial
# Our own commands
[prev lesson](Python-classes-and-objects) | [next lesson]()
[prev lesson](Python-classes-and-objects) | [next lesson](More-on-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.
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
@ -10,16 +13,17 @@ In Evennia, a Command is a Python _class_. If you are unsure about what a class
previous lesson. A Command inherits from `evennia.Command` or from one of the alternative command-
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
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.
combat, another for building etc. By default, Evennia groups all character-commands into one
big cmdset.
Command-Sets are then associated with objects. 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
- Putting a CmdSet on an object makes all commands in it available to the object
- CmdSets are stored on objects - this defines which commands are available to that object.
## Creating a custom command
@ -373,271 +377,14 @@ you could try to hit it (if you dare):
You won't see the second string. Only Smaug sees that (and is not amused).
## More advanced parsing
Let's expand our simple `hit` command to accept a little more complex input:
## Summary
hit <target> [[with] <weapon>]
That is, we want to support all of these forms
In this lesson we learned how to create our own Command, add it to a CmdSet and then ourselves.
We also upset a dragon.
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`:
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.
```python
#...
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.
```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.
```
- **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 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):
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 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
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!
## Adding the Command to a default Cmdset
For now, let's drop MyCmdSet:
> py self.cmdset.remove("commands.mycommands.MyCmdSet")
The command is not available to use until it is part of a [Command Set](../../../Component/Command-Sets). In this
example we will go the easiest route and add it to the default Character commandset that already
exists.
1. Edit `mygame/commands/default_cmdsets.py`
1. Import your new command with `from commands.command import CmdEcho`.
1. Add a line `self.add(CmdEcho())` to `CharacterCmdSet`, in the `at_cmdset_creation` method (the
template tells you where).
This is approximately how it should look at this point:
```python
# file mygame/commands/default_cmdsets.py
#[...]
from commands.command import CmdEcho
#[...]
class CharacterCmdSet(default_cmds.CharacterCmdSet):
key = "DefaultCharacter"
def at_cmdset_creation(self):
# this first adds all default commands
super(DefaultSet, self).at_cmdset_creation()
# all commands added after this point will extend or
# overwrite the default commands.
self.add(CmdEcho())
```
Next, run the `@reload` command. You should now be able to use your new `echo` command from inside
the game. Use `help echo` to see the documentation for the command.
If you have trouble, make sure to check the log for error messages (probably due to syntax errors in
your command definition).
> Note: Typing `echotest` will also work. It will be handled as the command `echo` directly followed
by
its argument `test` (which will end up in `self.args). To change this behavior, you can add the
`arg_regex` property alongside `key`, `help_category` etc. [See the arg_regex
documentation](Commands#on-arg_regex) for more info.
If you want to overload existing default commands (such as `look` or `get`), just add your new
command with the same key as the old one - it will then replace it. Just remember that you must use
`@reload` to see any changes.
See [Commands](../../../Component/Commands) for many more details and possibilities when defining Commands and using
Cmdsets in various ways.
## Adding the command to specific object types
Adding your Command to the `CharacterCmdSet` is just one easy exapmple. The cmdset system is very
generic. You can create your own cmdsets (let's say in a module `mycmdsets.py`) and add them to
objects as you please (how to control their merging is described in detail in the [Command Set
documentation](Command-Sets)).
```python
# file mygame/commands/mycmdsets.py
#[...]
from commands.command import CmdEcho
from evennia import CmdSet
#[...]
class MyCmdSet(CmdSet):
key = "MyCmdSet"
def at_cmdset_creation(self):
self.add(CmdEcho())
```
Now you just need to add this to an object. To test things (as superuser) you can do
@py self.cmdset.add("mycmdsets.MyCmdSet")
This will add this cmdset (along with its echo command) to yourself so you can test it. Note that
you cannot add a single Command to an object on its own, it must be part of a CommandSet in order to
do so.
The Command you added is not there permanently at this point. If you do a `@reload` the merger will
be gone. You *could* add the `permanent=True` keyword to the `cmdset.add` call. This will however
only make the new merged cmdset permanent on that *single* object. Often you want *all* objects of
this particular class to have this cmdset.
To make sure all new created objects get your new merged set, put the `cmdset.add` call in your
custom [Typeclasses](../../../Component/Typeclasses)' `at_object_creation` method:
```python
# e.g. in mygame/typeclasses/objects.py
from evennia import DefaultObject
class MyObject(DefaultObject):
def at_object_creation(self):
"called when the object is first created"
self.cmdset.add("mycmdset.MyCmdSet", permanent=True)
```
All new objects of this typeclass will now start with this cmdset and it will survive a `@reload`.
*Note:* An important caveat with this is that `at_object_creation` is only called *once*, when the
object is first created. This means that if you already have existing objects in your databases
using that typeclass, they will not have been initiated the same way. There are many ways to update
them; since it's a one-time update you can usually just simply loop through them. As superuser, try
the following:
@py from typeclasses.objects import MyObject; [o.cmdset.add("mycmdset.MyCmdSet") for o in
MyObject.objects.all()]
This goes through all objects in your database having the right typeclass, adding the new cmdset to
each. The good news is that you only have to do this if you want to post-add *cmdsets*. If you just
want to add a new *command*, you can simply add that command to the cmdset's `at_cmdset_creation`
and `@reload` to make the Command immediately available.
## Change where Evennia looks for command sets
Evennia uses settings variables to know where to look for its default command sets. These are
normally not changed unless you want to re-organize your game folder in some way. For example, the
default character cmdset defaults to being defined as
CMDSET_CHARACTER="commands.default_cmdset.CharacterCmdSet"
[prev lesson](Python-classes-and-objects) | [next lesson]()
[prev lesson](Python-classes-and-objects) | [next lesson](More-on-Commands)