mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Continued with adding commands
This commit is contained in:
parent
131a9f89aa
commit
830f793aa4
5 changed files with 843 additions and 627 deletions
|
|
@ -1,171 +0,0 @@
|
|||
# Adding Command Tutorial
|
||||
|
||||
This is a quick first-time tutorial expanding on the [Commands](../../Component/Commands) documentation.
|
||||
|
||||
Let's assume you have just downloaded Evennia, installed it and created your game folder (let's call
|
||||
it just `mygame` here). Now you want to try to add a new command. This is the fastest way to do it.
|
||||
|
||||
## Step 1: Creating a custom command
|
||||
|
||||
1. Open `mygame/commands/command.py` in a text editor. This is just one place commands could be
|
||||
placed but you get it setup from the onset as an easy place to start. It also already contains some
|
||||
example code.
|
||||
1. Create a new class in `command.py` inheriting from `default_cmds.MuxCommand`. Let's call it
|
||||
`CmdEcho` in this example.
|
||||
1. Set the class variable `key` to a good command name, like `echo`.
|
||||
1. Give your class a useful _docstring_. A docstring is the string at the very top of a class or
|
||||
function/method. The docstring at the top of the command class is read by Evennia to become the help
|
||||
entry for the Command (see
|
||||
[Command Auto-help](../../Component/Help-System#command-auto-help-system)).
|
||||
1. Define a class method `func(self)` that echoes your input back to you.
|
||||
|
||||
Below is an example how this all could look for the echo command:
|
||||
|
||||
```python
|
||||
# file mygame/commands/command.py
|
||||
#[...]
|
||||
from evennia import default_cmds
|
||||
class CmdEcho(default_cmds.MuxCommand):
|
||||
"""
|
||||
Simple command example
|
||||
|
||||
Usage:
|
||||
echo [text]
|
||||
|
||||
This command simply echoes text back to the caller.
|
||||
"""
|
||||
|
||||
key = "echo"
|
||||
|
||||
def func(self):
|
||||
"This actually does things"
|
||||
if not self.args:
|
||||
self.caller.msg("You didn't enter anything!")
|
||||
else:
|
||||
self.caller.msg("You gave the string: '%s'" % self.args)
|
||||
```
|
||||
|
||||
## Step 2: Adding the Command to a default Cmdset
|
||||
|
||||
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"
|
||||
|
||||
See `evennia/settings_default.py` for the other settings.
|
||||
643
docs/source/Howto/Starting/Part1/Adding-Commands.md
Normal file
643
docs/source/Howto/Starting/Part1/Adding-Commands.md
Normal file
|
|
@ -0,0 +1,643 @@
|
|||
# Adding Command Tutorial
|
||||
|
||||
[prev lesson](Python-classes-and-objects) | [next lesson]()
|
||||
|
||||
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.
|
||||
|
||||
In Evennia, a Command is a Python _class_. If you are unsure about what a class is, review the
|
||||
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
|
||||
as a bag holding many different commands. One CmdSet could for example hold all commands for
|
||||
combat, another for building etc.
|
||||
|
||||
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
|
||||
|
||||
## Creating a custom command
|
||||
|
||||
Open `mygame/commands/command.py`:
|
||||
|
||||
```python
|
||||
"""
|
||||
(module docstring)
|
||||
"""
|
||||
|
||||
from evennia import Command as BaseCommand
|
||||
# from evennia import default_cmds
|
||||
|
||||
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.
|
||||
|
||||
> The commented out `default_cmds` gives us access to Evennia's default commands for easy overriding. We'll try
|
||||
> 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:
|
||||
|
||||
```python
|
||||
|
||||
from commands.command import Command
|
||||
|
||||
class CmdEcho(Command):
|
||||
key = "echo"
|
||||
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
|
||||
```python
|
||||
|
||||
from commands.command import Command
|
||||
from evennia import CmdSet
|
||||
|
||||
class CmdEcho(Command):
|
||||
key = "echo"
|
||||
|
||||
|
||||
class MyCmdSet(CmdSet):
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdEcho)
|
||||
|
||||
```
|
||||
|
||||
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
|
||||
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
|
||||
"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:
|
||||
|
||||
Command echo has no defined `func()` - showing on-command variables:
|
||||
obj (<class 'typeclasses.characters.Character'>): YourName
|
||||
lockhandler (<class 'evennia.locks.lockhandler.LockHandler'>): cmd:all()
|
||||
caller (<class 'typeclasses.characters.Character'>): YourName
|
||||
cmdname (<class 'str'>): echo
|
||||
raw_cmdname (<class 'str'>): echo
|
||||
cmdstring (<class 'str'>): echo
|
||||
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,
|
||||
look, menutest, mudinfo, mvattr, name, nick, objects, ooc, open, option, page, password, perm, pose, public, py, quell, quit, reload, reset, rss2chan, say,
|
||||
script, scripts, server, service, sessions, set, setdesc, sethelp, sethome, shutdown, spawn, style, tag, tel, test2010, test2028, testrename, testtable,
|
||||
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
|
||||
|
||||
--------------------------------------------------
|
||||
echo - Command variables from evennia:
|
||||
--------------------------------------------------
|
||||
name of cmd (self.key): echo
|
||||
cmd aliases (self.aliases): []
|
||||
cmd locks (self.locks): cmd:all();
|
||||
help category (self.help_category): General
|
||||
object calling (self.caller): Griatch
|
||||
object storing cmdset (self.obj): Griatch
|
||||
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:
|
||||
|
||||
- `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
|
||||
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
|
||||
looks for to figure out what a Command actually does. Modify your `CmdEcho` class:
|
||||
|
||||
```python
|
||||
# ...
|
||||
|
||||
class CmdEcho(Command):
|
||||
"""
|
||||
A simple echo command
|
||||
|
||||
Usage:
|
||||
echo <something>
|
||||
|
||||
"""
|
||||
key = "echo"
|
||||
|
||||
def func(self):
|
||||
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
|
||||
[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.
|
||||
|
||||
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: ''
|
||||
|
||||
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:
|
||||
|
||||
> 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:
|
||||
|
||||
```python
|
||||
# ...
|
||||
|
||||
class CmdEcho(Command):
|
||||
"""
|
||||
A simple echo command
|
||||
|
||||
Usage:
|
||||
echo <something>
|
||||
|
||||
"""
|
||||
key = "echo"
|
||||
|
||||
def func(self):
|
||||
self.caller.msg(f"Echo: '{self.args.strip()}'")
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
The only difference is that we called `.strip()` on `self.args`. This is a helper method available on all
|
||||
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
|
||||
> 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.
|
||||
|
||||
### 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:
|
||||
|
||||
> 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
|
||||
needing to re-add the MyCmdSet again. To remove the cmdset again, do
|
||||
|
||||
> py self.cmdset.remove("commands.mycommands.MyCmdSet")
|
||||
|
||||
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:
|
||||
|
||||
> hit <target>
|
||||
You hit <target> with full force!
|
||||
|
||||
Not only that, we want the <target> to see
|
||||
|
||||
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.
|
||||
|
||||
Still in `mygame/commands/mycommands.py`, add a new class, between `CmdEcho` and `MyCmdSet`.
|
||||
|
||||
```python
|
||||
# ...
|
||||
|
||||
class CmdHit(Command):
|
||||
"""
|
||||
Hit a target.
|
||||
|
||||
Usage:
|
||||
hit <target>
|
||||
|
||||
"""
|
||||
key = "hit"
|
||||
|
||||
def func(self):
|
||||
args = self.args.strip()
|
||||
if not args:
|
||||
self.caller.msg("Who do you want to hit?")
|
||||
return
|
||||
target = self.caller.search(args)
|
||||
if not target:
|
||||
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!")
|
||||
# ...
|
||||
|
||||
```
|
||||
|
||||
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
|
||||
`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
|
||||
|
||||
if condition:
|
||||
...
|
||||
elif othercondition:
|
||||
...
|
||||
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.
|
||||
|
||||
```
|
||||
- **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":
|
||||
- `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`
|
||||
- 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".
|
||||
|
||||
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:
|
||||
- **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 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`.
|
||||
- **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.
|
||||
|
||||
```python
|
||||
# ...
|
||||
|
||||
class MyCmdSet(CmdSet):
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdEcho)
|
||||
self.add(CmdHit)
|
||||
|
||||
```
|
||||
|
||||
```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
|
||||
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:
|
||||
|
||||
> reload
|
||||
hit
|
||||
Who do you want to hit?
|
||||
hit me
|
||||
You hit YourName with full force!
|
||||
You got hit by YourName with full force!
|
||||
|
||||
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
|
||||
You hit Smaug with full force!
|
||||
|
||||
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:
|
||||
|
||||
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`:
|
||||
|
||||
|
||||
```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]()
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Continuing on with Python and Evennia
|
||||
# Python Classes and Evennia Typeclasses
|
||||
|
||||
[prev lesson](Gamedir-Overview) | [next lesson]()
|
||||
[prev lesson](Gamedir-Overview) | [next lesson](Adding-Commands)
|
||||
|
||||
We have now learned how to run some simple Python code from inside (and outside) your game server.
|
||||
We have also taken a look at what our game dir looks and what is where. Now we'll start to use it.
|
||||
|
|
@ -291,518 +291,256 @@ objects in turn:
|
|||
- The current policy positions of a political party
|
||||
- A rule with methods for resolving challenges or roll dice
|
||||
- A multi-dimenstional data-point for a complex economic simulation
|
||||
- And so much more!
|
||||
|
||||
### Classes can have children
|
||||
|
||||
Classes can _inherit_ from each other. A "child" class will inherit everything from its "parent" class. But if
|
||||
the child adds something with the same name as its parent, it will _override_ whatever it got from its parent.
|
||||
|
||||
Let's expand `mygame/typeclasses/mymobile.py` with another class:
|
||||
|
||||
```python
|
||||
|
||||
class Mobile:
|
||||
"""
|
||||
This is a base class for Mobiles.
|
||||
"""
|
||||
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
|
||||
def move_around(self):
|
||||
print(f"{self.key} is moving!")
|
||||
|
||||
|
||||
class Dragon(Mobile):
|
||||
"""
|
||||
This is a dragon-specific mobile.
|
||||
"""
|
||||
|
||||
def move_around(self):
|
||||
print(f"{self.key} flies through the air high above!")
|
||||
|
||||
[In the first part](Python-basic-introduction) of this Python-for-Evennia basic tutorial we learned
|
||||
how to run some simple Python code from inside the game. We also made our first new *module*
|
||||
containing a *function* that we called. Now we're going to start exploring the very important
|
||||
subject of *objects*.
|
||||
|
||||
**Contents:**
|
||||
- [On the subject of objects](Python-basic-tutorial-part-two#on-the-subject-of-objects)
|
||||
- [Exploring the Evennia library](Python-basic-tutorial-part-two#exploring-the-evennia-library)
|
||||
- [Tweaking our Character class](Python-basic-tutorial-part-two#tweaking-our-character-class)
|
||||
- [The Evennia shell](Python-basic-tutorial-part-two#the-evennia-shell)
|
||||
- [Where to go from here](Python-basic-tutorial-part-two#where-to-go-from-here)
|
||||
|
||||
### On the subject of objects
|
||||
|
||||
In the first part of the tutorial we did things like
|
||||
|
||||
> py me.msg("Hello World!")
|
||||
|
||||
To learn about functions and imports we also passed that `me` on to a function `hello_world` in
|
||||
another module.
|
||||
|
||||
Let's learn some more about this `me` thing we are passing around all over the place. In the
|
||||
following we assume that we named our superuser Character "Christine".
|
||||
|
||||
> py me
|
||||
Christine
|
||||
> py me.key
|
||||
Christine
|
||||
|
||||
These returns look the same at first glance, but not if we examine them more closely:
|
||||
|
||||
> py type(me)
|
||||
<class 'typeclasses.characters.Character'>
|
||||
> py type(me.key)
|
||||
<type str>
|
||||
|
||||
> Note: In some MU clients, such as Mudlet and MUSHclient simply returning `type(me)`, you may not
|
||||
see the proper return from the above commands. This is likely due to the HTML-like tags `<...>`,
|
||||
being swallowed by the client.
|
||||
|
||||
The `type` function is, like `print`, another in-built function in Python. It
|
||||
tells us that we (`me`) are of the *class* `typeclasses.characters.Character`.
|
||||
Meanwhile `me.key` is a *property* on us, a string. It holds the name of this
|
||||
object.
|
||||
|
||||
> When you do `py me`, the `me` is defined in such a way that it will use its `.key` property to
|
||||
represent itself. That is why the result is the same as when doing `py me.key`. Also, remember that
|
||||
as noted in the first part of the tutorial, the `me` is *not* a reserved Python word; it was just
|
||||
defined by the Evennia developers as a convenient short-hand when creating the `py` command. So
|
||||
don't expect `me` to be available elsewhere.
|
||||
|
||||
A *class* is like a "factory" or blueprint. From a class you then create individual *instances*. So
|
||||
if class is`Dog`, an instance of `Dog` might be `fido`. Our in-game persona is of a class
|
||||
`Character`. The superuser `christine` is an *instance* of the `Character` class (an instance is
|
||||
also often referred to as an *object*). This is an important concept in *object oriented
|
||||
programming*. You are wise to [familiarize yourself with it](https://en.wikipedia.org/wiki/Class-
|
||||
based_programming) a little.
|
||||
|
||||
> In other terms:
|
||||
> * class: A description of a thing, all the methods (code) and data (information)
|
||||
> * object: A thing, defined as an *instance* of a class.
|
||||
>
|
||||
> So in "Fido is a Dog", "Fido" is an object--a unique thing--and "Dog" is a class. Coders would
|
||||
also say, "Fido is an instance of Dog". There can be other dogs too, such as Butch and Fifi. They,
|
||||
too, would be instances of Dog.
|
||||
>
|
||||
> As another example: "Christine is a Character", or "Christine is an instance of
|
||||
typeclasses.characters.Character". To start, all characters will be instances of
|
||||
typeclass.characters.Character.
|
||||
>
|
||||
> You'll be writing your own class soon! The important thing to know here is how classes and objects
|
||||
relate.
|
||||
|
||||
The string `'typeclasses.characters.Character'` we got from the `type()` function is not arbitrary.
|
||||
You'll recognize this from when we _imported_ `world.test` in part one. This is a _path_ exactly
|
||||
describing where to find the python code describing this class. Python treats source code files on
|
||||
your hard drive (known as *modules*) as well as folders (known as *packages*) as objects that you
|
||||
access with the `.` operator. It starts looking at a place that Evennia has set up for you - namely
|
||||
the root of your own game directory.
|
||||
|
||||
Open and look at your game folder (named `mygame` if you exactly followed the Getting Started
|
||||
instructions) in a file editor or in a new terminal/console. Locate the file
|
||||
`mygame/typeclasses/characters.py`
|
||||
def firebreath(self):
|
||||
"""
|
||||
Let our dragon breathe fire.
|
||||
"""
|
||||
print(f"{self.key} breathes fire!")
|
||||
|
||||
```
|
||||
mygame/
|
||||
typeclasses
|
||||
characters.py
|
||||
|
||||
We added some docstrings for clarity. It's always a good idea to add doc strings; you can do so also for methods,
|
||||
as exemplified for the new `firebreath` method.
|
||||
|
||||
We created the new class `Dragon` but we also specified that `Mobile` is the _parent_ of `Dragon` but adding
|
||||
the parent in parenthesis. `class Classname(Parent)` is the way to do this.
|
||||
|
||||
```sidebar:: Multi-inheritance
|
||||
|
||||
It's possible to add more comma-separated parents to a class. You should usually avoid
|
||||
this until you `really` know what you are doing. A single parent will be enough for almost
|
||||
every case you'll need.
|
||||
|
||||
```
|
||||
|
||||
This represents the first part of the python path - `typeclasses.characters` (the `.py` file ending
|
||||
is never included in the python path). The last bit, `.Character` is the actual class name inside
|
||||
the `characters.py` module. Open that file in a text editor and you will see something like this:
|
||||
Let's try out our new class. First `reload` the server and the do
|
||||
|
||||
> py
|
||||
> from typeclasses.mobile import Dragon
|
||||
> smaug = Dragon("Smaug")
|
||||
> smaug.move_around()
|
||||
Smaug flies through the air high above!
|
||||
> smaug.firebreath()
|
||||
Smaug breathes fire!
|
||||
|
||||
Because we didn't implement `__init__` in `Dragon`, we got the one from `Monster` instead. But since we
|
||||
implemented our own `move_around` in `Dragon`, it _overrides_ the one in `Monster`. And `firebreath` is only
|
||||
available for `Dragon`s of course. Having that on `Monster` would not have made much sense, since not every monster
|
||||
can breathe fire.
|
||||
|
||||
One can also force a class to use resources from the parent even if you are overriding some of it. This is done
|
||||
with the `super()` method. Modify your `Dragon` class as follows:
|
||||
|
||||
|
||||
```python
|
||||
# ...
|
||||
|
||||
class Dragon(Monster):
|
||||
|
||||
def move_around(self):
|
||||
super().move_around()
|
||||
print("The world trembles.")
|
||||
|
||||
# ...
|
||||
```
|
||||
> Keep `Monster` and the `firebreath` method, `# ...` indicates the rest of the code is untouched.
|
||||
>
|
||||
|
||||
The `super().move_around()` line means that we are calling `move_around()` on the parent of the class. So in this
|
||||
case, we will call `Monster.move_around` first, before doing our own thing.
|
||||
|
||||
Now `reload` the server and then:
|
||||
|
||||
> py
|
||||
> from typeclasses.mobile import Dragon
|
||||
> smaug = Dragon("Smaug")
|
||||
> smaug.move_around()
|
||||
Smaug is moving!
|
||||
The world trembles.
|
||||
|
||||
We can see that `Monster.move_around()` is calls first and prints "Smaug is moving!", followed by the extra bit
|
||||
about the trembling world we added in the `Dragon` class.
|
||||
|
||||
Inheritance is very powerful because it allows you to organize and re-use code while only adding the special things
|
||||
you want to change. Evennia uses this concept a lot.
|
||||
|
||||
|
||||
## Our first persistent object
|
||||
|
||||
Now we should know enough to understand what is happening in `mygame/typeclasses/objects.py`.
|
||||
Open it again:
|
||||
|
||||
```python
|
||||
"""
|
||||
(Doc string for module)
|
||||
module docstring
|
||||
"""
|
||||
from evennia import DefaultObject
|
||||
|
||||
from evennia import DefaultCharacter
|
||||
|
||||
class Character(DefaultCharacter):
|
||||
class Object(DefaultObject):
|
||||
"""
|
||||
(Doc string for class)
|
||||
class docstring
|
||||
"""
|
||||
pass
|
||||
|
||||
```
|
||||
|
||||
There is `Character`, the last part of the path. Note how empty this file is. At first glance one
|
||||
would think a Character had no functionality at all. But from what we have used already we know it
|
||||
has at least the `key` property and the method `msg`! Where is the code? The answer is that this
|
||||
'emptiness' is an illusion caused by something called *inheritance*. Read on.
|
||||
So we have a class `Object` that _inherits_ from `DefaultObject`, which we have imported from Evennia.
|
||||
The class itself doesn't do anything (it just `pass`es) but that doesn't mean it's useless. As we've seen,
|
||||
it inherits all the functionality of its parent. It's in fact an _exact replica_ of `DefaultObject` right now.
|
||||
If we knew what kind of methods and resources were available on `DefaultObject` we could add our own and
|
||||
change the way it works!
|
||||
|
||||
Firstly, in the same way as the little `hello.py` we did in the first part of the tutorial, this is
|
||||
an example of full, multi-line Python code. Those triple-quoted strings are used for strings that
|
||||
have line breaks in them. When they appear on their own like this, at the top of a python module,
|
||||
class or similar they are called *doc strings*. Doc strings are read by Python and is used for
|
||||
producing online help about the function/method/class/module. By contrast, a line starting with `#`
|
||||
is a *comment*. It is ignored completely by Python and is only useful to help guide a human to
|
||||
understand the code.
|
||||
> Hint: We will get back to this, but to learn what resources an Evennia parent like `DefaultObject` offers,
|
||||
> easiest is to peek at its [API documentation](api:evennia.objects.objects#DefaultObject). The docstring for
|
||||
> the `Object` class can also help.
|
||||
|
||||
The line
|
||||
One thing that Evennia offers and which you don't get with vanilla Python classes is _persistence_. As you've
|
||||
found, Fluffy, Cuddly and Smaug are gone once we reload the server. Let's see if we can fix this.
|
||||
|
||||
```python
|
||||
class Character(DefaultCharacter):
|
||||
```
|
||||
|
||||
means that the class `Character` is a *child* of the class `DefaultCharacter`. This is called
|
||||
*inheritance* and is another fundamental concept. The answer to the question "where is the code?" is
|
||||
that the code is *inherited* from its parent, `DefaultCharacter`. And that in turn may inherit code
|
||||
from *its* parent(s) and so on. Since our child, `Character` is empty, its functionality is *exactly
|
||||
identical* to that of its parent. The moment we add new things to Character, these will take
|
||||
precedence. And if we add something that already existed in the parent, our child-version will
|
||||
*override* the version in the parent. This is very practical: It means that we can let the parent do
|
||||
the heavy lifting and only tweak the things we want to change. It also means that we could easily
|
||||
have many different Character classes, all inheriting from `DefaultCharacter` but changing different
|
||||
things. And those can in turn also have children ...
|
||||
|
||||
Let's go on an expedition up the inheritance tree.
|
||||
|
||||
### Exploring the Evennia library
|
||||
|
||||
Let's figure out how to tweak `Character`. Right now we don't know much about `DefaultCharacter`
|
||||
though. Without knowing that we won't know what to override. At the top of the file you find
|
||||
|
||||
```python
|
||||
from evennia import DefaultCharacter
|
||||
```
|
||||
|
||||
This is an `import` statement again, but on a different form to what we've seen before. `from ...
|
||||
import ...` is very commonly used and allows you to precisely dip into a module to extract just the
|
||||
component you need to use. In this case we head into the `evennia` package to get
|
||||
`DefaultCharacter`.
|
||||
|
||||
Where is `evennia`? To find it you need to go to the `evennia` folder (repository) you originally
|
||||
cloned from us. If you open it, this is how it looks:
|
||||
|
||||
```
|
||||
evennia/
|
||||
__init__.py
|
||||
bin/
|
||||
CHANGELOG.txt etc.
|
||||
...
|
||||
evennia/
|
||||
...
|
||||
```
|
||||
There are lots of things in there. There are some docs but most of those have to do with the
|
||||
distribution of Evennia and does not concern us right now. The `evennia` subfolder is what we are
|
||||
looking for. *This* is what you are accessing when you do `from evennia import ...`. It's set up by
|
||||
Evennia as a good place to find modules when the server starts. The exact layout of the Evennia
|
||||
library [is covered by our directory overview](Directory-Overview#evennia-library-layout). You can
|
||||
also explore it [online on github](https://github.com/evennia/evennia/tree/master/evennia).
|
||||
|
||||
The structure of the library directly reflects how you import from it.
|
||||
|
||||
- To, for example, import [the text justify
|
||||
function](https://github.com/evennia/evennia/blob/master/evennia/utils/utils.py#L201) from
|
||||
`evennia/utils/utils.py` you would do `from evennia.utils.utils import justify`. In your code you
|
||||
could then just call `justify(...)` to access its functionality.
|
||||
- You could also do `from evennia.utils import utils`. In code you would then have to write
|
||||
`utils.justify(...)`. This is practical if want a lot of stuff from that `utils.py` module and don't
|
||||
want to import each component separately.
|
||||
- You could also do `import evennia`. You would then have to enter the full
|
||||
`evennia.utils.utils.justify(...)` every time you use it. Using `from` to only import the things you
|
||||
need is usually easier and more readable.
|
||||
- See [this overview](http://effbot.org/zone/import-confusion.htm) about the different ways to
|
||||
import in Python.
|
||||
|
||||
Now, remember that our `characters.py` module did `from evennia import DefaultCharacter`. But if we
|
||||
look at the contents of the `evennia` folder, there is no `DefaultCharacter` anywhere! This is
|
||||
because Evennia gives a large number of optional "shortcuts", known as [the "flat" API](Evennia-
|
||||
API). The intention is to make it easier to remember where to find stuff. The flat API is defined in
|
||||
that weirdly named `__init__.py` file. This file just basically imports useful things from all over
|
||||
Evennia so you can more easily find them in one place.
|
||||
|
||||
We could [just look at the documenation](github:evennia#typeclasses) to find out where we can look
|
||||
at our `DefaultCharacter` parent. But for practice, let's figure it out. Here is where
|
||||
`DefaultCharacter` [is imported
|
||||
from](https://github.com/evennia/evennia/blob/master/evennia/__init__.py#L188) inside `__init__.py`:
|
||||
|
||||
```python
|
||||
from .objects.objects import DefaultCharacter
|
||||
```
|
||||
|
||||
The period at the start means that it imports beginning from the same location this module sits(i.e.
|
||||
the `evennia` folder). The full python-path accessible from the outside is thus
|
||||
`evennia.objects.objects.DefaultCharacter`. So to import this into our game it'd be perfectly valid
|
||||
to do
|
||||
|
||||
```python
|
||||
from evennia.objects.objects import DefaultCharacter
|
||||
```
|
||||
|
||||
Using
|
||||
|
||||
```python
|
||||
from evennia import DefaultCharacter
|
||||
```
|
||||
|
||||
is the same thing, just a little easier to remember.
|
||||
|
||||
> To access the shortcuts of the flat API you *must* use `from evennia import
|
||||
> ...`. Using something like `import evennia.DefaultCharacter` will not work.
|
||||
> See [more about the Flat API here](../../../Evennia-API).
|
||||
|
||||
|
||||
### Tweaking our Character class
|
||||
|
||||
In the previous section we traced the parent of our `Character` class to be
|
||||
`DefaultCharacter` in
|
||||
[evennia/objects/objects.py](https://github.com/evennia/evennia/blob/master/evennia/objects/objects.py).
|
||||
Open that file and locate the `DefaultCharacter` class. It's quite a bit down
|
||||
in this module so you might want to search using your editor's (or browser's)
|
||||
search function. Once you find it, you'll find that the class starts like this:
|
||||
Go back to `mygame/typeclasses/mobile.py`. Change it as follows:
|
||||
|
||||
```python
|
||||
|
||||
class DefaultCharacter(DefaultObject):
|
||||
from typeclasses.objects import Object
|
||||
|
||||
class Mobile(Object):
|
||||
"""
|
||||
This implements an Object puppeted by a Session - that is, a character
|
||||
avatar controlled by an account.
|
||||
This is a base class for Mobiles.
|
||||
"""
|
||||
def move_around(self):
|
||||
print(f"{self.key} is moving!")
|
||||
|
||||
|
||||
class Dragon(Mobile):
|
||||
"""
|
||||
This is a dragon-specific mobile.
|
||||
"""
|
||||
|
||||
def basetype_setup(self):
|
||||
"""
|
||||
Setup character-specific security.
|
||||
You should normally not need to overload this, but if you do,
|
||||
make sure to reproduce at least the two last commands in this
|
||||
method (unless you want to fundamentally change how a
|
||||
Character object works).
|
||||
"""
|
||||
super().basetype_setup()
|
||||
self.locks.add(";".join(["get:false()", # noone can pick up the character
|
||||
"call:false()"])) # no commands can be called on character from
|
||||
outside
|
||||
# add the default cmdset
|
||||
self.cmdset.add_default(settings.CMDSET_CHARACTER, permanent=True)
|
||||
def move_around(self):
|
||||
super().move_around()
|
||||
print("The world trembles.")
|
||||
|
||||
def at_after_move(self, source_location, **kwargs):
|
||||
def firebreath(self):
|
||||
"""
|
||||
Let our dragon breathe fire.
|
||||
"""
|
||||
We make sure to look around after a move.
|
||||
"""
|
||||
if self.location.access(self, "view"):
|
||||
self.msg(self.at_look(self.location))
|
||||
|
||||
def at_pre_puppet(self, account, session=None, **kwargs):
|
||||
"""
|
||||
Return the character from storage in None location in `at_post_unpuppet`.
|
||||
"""
|
||||
|
||||
# ...
|
||||
print(f"{self.key} breathes fire!")
|
||||
|
||||
```
|
||||
|
||||
... And so on (you can see the full [class online
|
||||
here](https://github.com/evennia/evennia/blob/master/evennia/objects/objects.py#L1915)). Here we
|
||||
have functional code! These methods may not be directly visible in `Character` back in our game dir,
|
||||
but they are still available since `Character` is a child of `DefaultCharacter` above. Here is a
|
||||
brief summary of the methods we find in `DefaultCharacter` (follow in the code to see if you can see
|
||||
roughly where things happen)::
|
||||
Don't forget to save. We removed `Monster.__init__` and made `Mobile` inherit from Evennia's `Object` (which in turn
|
||||
inherits from Evennia's `DefaultObject`, as we saw). By extension, this means that `Dragon` also inherits
|
||||
from `DefaultObject`, just from further away!
|
||||
|
||||
- `basetype_setup` is called by Evennia only once, when a Character is first created. In the
|
||||
`DefaultCharacter` class it sets some particular [Locks](../../../Component/Locks) so that people can't pick up and
|
||||
puppet Characters just like that. It also adds the [Character Cmdset](../../../Component/Command-Sets) so that
|
||||
Characters always can accept command-input (this should usually not be modified - the normal hook to
|
||||
override is `at_object_creation`, which is called after `basetype_setup` (it's in the parent)).
|
||||
- `at_after_move` makes it so that every time the Character moves, the `look` command is
|
||||
automatically fired (this would not make sense for just any regular Object).
|
||||
- `at_pre_puppet` is called when an Account begins to puppet this Character. When not puppeted, the
|
||||
Character is hidden away to a `None` location. This brings it back to the location it was in before.
|
||||
Without this, "headless" Characters would remain in the game world just standing around.
|
||||
- `at_post_puppet` is called when puppeting is complete. It echoes a message to the room that his
|
||||
Character has now connected.
|
||||
- `at_post_unpuppet` is called once stopping puppeting of the Character. This hides away the
|
||||
Character to a `None` location again.
|
||||
- There are also some utility properties which makes it easier to get some time stamps from the
|
||||
Character.
|
||||
### Creating by calling the class (less common way)
|
||||
|
||||
Reading the class we notice another thing:
|
||||
First reload the server as usual. We will need to create the dragon a little differently this time:
|
||||
|
||||
```sidebar:: Keyword arguments
|
||||
|
||||
Keyword arguments (like `db_key="Smaug"`) is a way to
|
||||
name the input arguments to a function or method. They make
|
||||
things easier to read but also allows for conveniently setting
|
||||
defaults for values not given explicitly.
|
||||
|
||||
```python
|
||||
class DefaultCharacter(DefaultObject):
|
||||
# ...
|
||||
```
|
||||
> py
|
||||
> from typeclasses.mymobile import Dragon
|
||||
> smaug = Dragon(db_key="Smaug", db_location=here)
|
||||
> smaug.save()
|
||||
> smaug.move_around()
|
||||
Smaug is moving!
|
||||
The world trembles.
|
||||
|
||||
This means that `DefaultCharacter` is in *itself* a child of something called `DefaultObject`! Let's
|
||||
see what this parent class provides. It's in the same module as `DefaultCharacter`, you just need to
|
||||
[scroll up near the
|
||||
top](https://github.com/evennia/evennia/blob/master/evennia/objects/objects.py#L182):
|
||||
Smaug works the same as before, but we created him differently: first we used
|
||||
`Dragon(db_key="Smaug", db_location=here)` to create the object, and then we used `smaug.save()` afterwards.
|
||||
|
||||
```python
|
||||
class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
|
||||
# ...
|
||||
```
|
||||
> quit()
|
||||
Python Console is closing.
|
||||
> look
|
||||
|
||||
You should now see that Smaug _is in the room with you_. Woah!
|
||||
|
||||
This is a really big class where the bulk of code defining an in-game object resides. It consists of
|
||||
a large number of methods, all of which thus also becomes available on the `DefaultCharacter` class
|
||||
below *and* by extension in your `Character` class over in your game dir. In this class you can for
|
||||
example find the `msg` method we have been using before.
|
||||
> reload
|
||||
> look
|
||||
|
||||
_He's still there_... What we just did is to create a new entry in the database for Smaug. We gave the object
|
||||
its name (key) and set its location to our current location (remember that `here` is just something available
|
||||
in the `py` command, you can't use it elsewhere).
|
||||
|
||||
> You should probably not expect to understand all details yet, but as an exercise, find and read
|
||||
the doc string of `msg`.
|
||||
To make use of Smaug in code we must first find him in the database. For an object in the current
|
||||
location we can easily do this in `py` by using `me.search()`:
|
||||
|
||||
> As seen, `DefaultObject` actually has multiple parents. In one of those the basic `key` property
|
||||
is defined, but we won't travel further up the inheritance tree in this tutorial. If you are
|
||||
interested to see them, you can find `TypeclassBase` in
|
||||
[evennia/typeclasses/models.py](https://github.com/evennia/evennia/blob/master/evennia/typeclasses/models.py#L93)
|
||||
and `ObjectDB` in
|
||||
[evennia/objects/models.py](https://github.com/evennia/evennia/blob/master/evennia/objects/models.py#L121).
|
||||
We will also not go into the details of [Multiple
|
||||
Inheritance](https://docs.python.org/2/tutorial/classes.html#multiple-inheritance) or
|
||||
[Metaclasses](http://www.onlamp.com/pub/a/python/2003/04/17/metaclasses.html) here. The general rule
|
||||
is that if you realize that you need these features, you already know enough to use them.
|
||||
> py smaug = me.search("Smaug") ; smaug.firebreath()
|
||||
Smaug breathes fire!
|
||||
|
||||
Remember the `at_pre_puppet` method we looked at in `DefaultCharacter`? If you look at the
|
||||
`at_pre_puppet` hook as defined in `DefaultObject` you'll find it to be completely empty (just a
|
||||
`pass`). So if you puppet a regular object it won't be hiding/retrieving the object when you
|
||||
unpuppet it. The `DefaultCharacter` class *overrides* its parent's functionality with a version of
|
||||
its own. And since it's `DefaultCharacter` that our `Character` class inherits back in our game dir,
|
||||
it's *that* version of `at_pre_puppet` we'll get. Anything not explicitly overridden will be passed
|
||||
down as-is.
|
||||
### Creating using create_object
|
||||
|
||||
While it's useful to read the code, we should never actually modify anything inside the `evennia`
|
||||
folder. Only time you would want that is if you are planning to release a bug fix or new feature for
|
||||
Evennia itself. Instead you *override* the default functionality inside your game dir.
|
||||
Creating Smaug like we did above is nice because it's similar to how we created non-database
|
||||
bound Python instances before. But you need to use `db_key` instead of `key` and you also have to
|
||||
remember to call `.save()` afterwards. Evennia has a helper function that is more common to use,
|
||||
called `create_object`:
|
||||
|
||||
So to conclude our little foray into classes, objects and inheritance, locate the simple little
|
||||
`at_before_say` method in the `DefaultObject` class:
|
||||
> py fluffy = evennia.create_object('typeclases.mymobile.Mobile', key="Fluffy", location=here)
|
||||
> look
|
||||
|
||||
Boom, Fluffy should now be in the room with you, a little less scary than Smaug. You specify the
|
||||
python-path to the code you want and then set the key and location. Evennia sets things up and saves for you.
|
||||
|
||||
```python
|
||||
def at_before_say(self, message, **kwargs):
|
||||
"""
|
||||
(doc string here)
|
||||
"""
|
||||
return message
|
||||
```
|
||||
If you want to find Fluffy from anywhere, you can use Evennia's `search_object` helper:
|
||||
|
||||
If you read the doc string you'll find that this can be used to modify the output of `say` before it
|
||||
goes out. You can think of it like this: Evennia knows the name of this method, and when someone
|
||||
speaks, Evennia will make sure to redirect the outgoing message through this method. It makes it
|
||||
ripe for us to replace with a version of our own.
|
||||
> fluffy = evennia.search_object("Fluffy")[0] ; fluffy.move_around()
|
||||
Fluffy is moving!
|
||||
|
||||
> In the Evennia documentation you may sometimes see the term *hook* used for a method explicitly
|
||||
meant to be overridden like this.
|
||||
> The `[0]` is because `search_object` always returns a _list_ of zero, one or more found objects. The `[0]`
|
||||
means that we want the first element of this list (counting in Python always starts from 0). If there were
|
||||
multiple Fluffies we could get the second one with `[1]`.
|
||||
|
||||
As you can see, the first argument to `at_before_say` is `self`. In Python, the first argument of a
|
||||
method is *always a back-reference to the object instance on which the method is defined*. By
|
||||
convention this argument is always called `self` but it could in principle be named anything. The
|
||||
`self` is very useful. If you wanted to, say, send a message to the same object from inside
|
||||
`at_before_say`, you would do `self.msg(...)`.
|
||||
### Creating using create-command
|
||||
|
||||
What can trip up newcomers is that you *don't* include `self` when you *call* the method. Try:
|
||||
Finally, you can also create a new Dragon using the familiar builder-commands we explored a few lessons ago:
|
||||
|
||||
> @py me.at_before_say("Hello World!")
|
||||
Hello World!
|
||||
> create/drop Cuddly:typeclasses.mymobile.Mobile
|
||||
|
||||
Note that we don't send `self` but only the `message` argument. Python will automatically add `self`
|
||||
for us. In this case, `self` will become equal to the Character instance `me`.
|
||||
Cuddly is now in the room. After learning about how objects are created you'll realize that all this command really
|
||||
does is to parse your input, figure out that `/drop` means to "give the object the same location as the caller",
|
||||
and then do a call akin to
|
||||
|
||||
By default the `at_before_say` method doesn't do anything. It just takes the `message` input and
|
||||
`return`s it just the way it was (the `return` is another reserved Python word).
|
||||
evennia.create_object("typeclasses.mymobile.Mobile", key="Cuddly", location=here)
|
||||
|
||||
> We won't go into `**kwargs` here, but it (and its sibling `*args`) is also important to
|
||||
understand, extra reading is [here for
|
||||
`**kwargs`](https://stackoverflow.com/questions/1769403/understanding-kwargs-in-python).
|
||||
That's pretty much all there is to the mighty `create` command.
|
||||
|
||||
Now, open your game folder and edit `mygame/typeclasses/characters.py`. Locate your `Character`
|
||||
class and modify it as such:
|
||||
... And speaking of Commands, we should try to add one of our own next.
|
||||
|
||||
```python
|
||||
class Character(DefaultCharacter):
|
||||
"""
|
||||
(docstring here)
|
||||
"""
|
||||
def at_before_say(self, message, **kwargs):
|
||||
"Called before say, allows for tweaking message"
|
||||
return f"{message} ..."
|
||||
```
|
||||
|
||||
So we add our own version of `at_before_say`, duplicating the `def` line from the parent but putting
|
||||
new code in it. All we do in this tutorial is to add an ellipsis (`...`) to the message as it passes
|
||||
through the method.
|
||||
|
||||
Note that `f` in front of the string, it means we turned the string into a 'formatted string'. We
|
||||
can now easily inject stuff directly into the string by wrapping them in curly brackets `{ }`. In
|
||||
this example, we put the incoming `message` into the string, followed by an ellipsis. This is only
|
||||
one way to format a string. Python has very powerful [string
|
||||
formatting](https://docs.python.org/2/library/string.html#format-specification-mini-language) and
|
||||
you are wise to learn it well, considering your game will be mainly text-based.
|
||||
|
||||
> You could also copy & paste the relevant method from `DefaultObject` here to get the full doc
|
||||
string. For more complex methods, or if you only want to change some small part of the default
|
||||
behavior, copy & pasting will eliminate the need to constantly look up the original method and keep
|
||||
you sane.
|
||||
|
||||
In-game, now try
|
||||
|
||||
> @reload
|
||||
> say Hello
|
||||
You say, "Hello ..."
|
||||
|
||||
An ellipsis `...` is added to what you said! This is a silly example but you have just made your
|
||||
first code change to core functionality - without touching any of Evennia's original code! We just
|
||||
plugged in our own version of the `at_before_say` method and it replaced the default one. Evennia
|
||||
happily redirected the message through our version and we got a different output.
|
||||
|
||||
> For sane overriding of parent methods you should also be aware of Python's
|
||||
[super](https://docs.python.org/3/library/functions.html#super), which allows you to call the
|
||||
methods defined on a parent in your child class.
|
||||
|
||||
### The Evennia shell
|
||||
|
||||
Now on to some generally useful tools as you continue learning Python and Evennia. We have so far
|
||||
explored using `py` and have inserted Python code directly in-game. We have also modified Evennia's
|
||||
behavior by overriding default functionality with our own. There is a third way to conveniently
|
||||
explore Evennia and Python - the Evennia shell.
|
||||
|
||||
Outside of your game, `cd` to your mygame folder and make sure any needed virtualenv is running.
|
||||
Next:
|
||||
|
||||
> pip install ipython # only needed once
|
||||
|
||||
The [`IPython`](https://en.wikipedia.org/wiki/IPython) program is just a nicer interface to the
|
||||
Python interpreter - you only need to install it once, after which Evennia will use it
|
||||
automatically.
|
||||
|
||||
> evennia shell
|
||||
|
||||
If you did this call from your game dir you will now be in a Python prompt managed by the IPython
|
||||
program.
|
||||
|
||||
IPython ...
|
||||
...
|
||||
In [1]:
|
||||
IPython has some very nice ways to explore what Evennia has to offer.
|
||||
|
||||
> import evennia
|
||||
> evennia.<TAB>
|
||||
|
||||
That is, write `evennia.` and press the Tab key. You will be presented with a list of all available
|
||||
resources in the Evennia Flat API. We looked at the `__init__.py` file in the `evennia` folder
|
||||
earlier, so some of what you see should be familiar. From the IPython prompt, do:
|
||||
|
||||
> from evennia import DefaultCharacter
|
||||
> DefaultCharacter.at_before_say?
|
||||
|
||||
Don't forget that you can use `<TAB>` to auto-complete code as you write. Appending a single `?` to
|
||||
the end will show you the doc-string for `at_before_say` we looked at earlier. Use `??` to get the
|
||||
whole source code.
|
||||
|
||||
Let's look at our over-ridden version instead. Since we started the `evennia shell` from our game
|
||||
dir we can easily get to our code too:
|
||||
|
||||
> from typeclasses.characters import Character
|
||||
> Character.at_before_say??
|
||||
|
||||
This will show us the changed code we just did. Having a window with IPython running is very
|
||||
convenient for quickly exploring code without having to go digging through the file structure!
|
||||
|
||||
### Where to go from here
|
||||
|
||||
This should give you a running start using Python with Evennia. If you are completely new to
|
||||
programming or Python you might want to look at a more formal Python tutorial. You can find links
|
||||
and resources [on our link page](../../../Links).
|
||||
|
||||
We have touched upon many of the concepts here but to use Evennia and to be able to follow along in
|
||||
the code, you will need basic understanding of Python
|
||||
[modules](http://docs.python.org/2/tutorial/modules.html),
|
||||
[variables](http://www.tutorialspoint.com/python/python_variable_types.htm), [conditional
|
||||
statements](http://docs.python.org/tutorial/controlflow.html#if-statements),
|
||||
[loops](http://docs.python.org/tutorial/controlflow.html#for-statements),
|
||||
[functions](http://docs.python.org/tutorial/controlflow.html#defining-functions), [lists,
|
||||
dictionaries, list comprehensions](http://docs.python.org/tutorial/datastructures.html) and [string
|
||||
formatting](http://docs.python.org/tutorial/introduction.html#strings). You should also have a basic
|
||||
understanding of [object-oriented
|
||||
programming](http://www.tutorialspoint.com/python/python_classes_objects.htm) and what Python
|
||||
[Classes](http://docs.python.org/tutorial/classes.html) are.
|
||||
|
||||
Once you have familiarized yourself, or if you prefer to pick Python up as you go, continue to one
|
||||
of the beginning-level [Evennia tutorials](Tutorials) to gradually build up your understanding.
|
||||
|
||||
Good luck!
|
||||
|
||||
[prev lesson](Gamedir-Overview) | [next lesson]()
|
||||
[prev lesson](Gamedir-Overview) | [next lesson](Adding-Commands)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue