mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Further updates
This commit is contained in:
parent
830f793aa4
commit
a515ececff
11 changed files with 856 additions and 540 deletions
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue