Updated HTML docs.

This commit is contained in:
Griatch 2022-11-15 19:43:25 +00:00
parent 59e50f3fa5
commit 06bc3c8bcd
663 changed files with 2 additions and 61705 deletions

View file

@ -1,394 +0,0 @@
# Adding custom 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
what is in it.
```{sidebar} Commands are not typeclassed
If you just came from the previous lesson, you might want to know that Commands and
CommandSets are not `typeclassed`. That is, instances of them are not saved to the
database. They are "just" normal Python classes.
```
In Evennia, a Command is a Python _class_. If you are unsure about what a class is, review the
previous lessons! A Command inherits from `evennia.Command` or from one of the alternative command-
classes, such as `MuxCommand` which is what most default commands use.
All Commands are in turn grouped in another class called a _Command Set_. Think of a Command Set
as a bag holding many different commands. One CmdSet could for example hold all commands for
combat, another for building etc. By default, Evennia groups all character-commands into one
big cmdset.
Command-Sets are then associated with objects, for example with your Character. Doing so makes the
commands in that cmdset available to the object. So, to summarize:
- Commands are classes
- A group of Commands is stored in a CmdSet
- CmdSets are stored on objects - this defines which commands are available to that 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](./Beginner-Tutorial-Python-basic-introduction.md), 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 _persistent_ change though:
> py self.cmdset.add("commands.mycommands.MyCmdSet", persistent=True)
Now you can `reload` as much as you want and your code changes will be available directly without
needing to re-add the MyCmdSet again. To remove the cmdset again, do
> py self.cmdset.remove("commands.mycommands.MyCmdSet")
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`.
```{code-block} python
:linenos:
# ...
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 3**: The normal `class` header. We inherit from `Command` which we imported at the top of this file.
- **Lines 4-10**: The docstring and help-entry for the command. You could expand on this as much as you wanted.
- **Line 11**: We want to write `hit` to use this command.
- **Line 14**: 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 15** 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 16-17**: This code will only run if the `if` statement is truthy, in this case if `args` is the empty string.
- **Line 17**: `return` is a reserved Python word that exits `func` immediately.
- **Line 18**: We use `self.caller.search` to look for the target in the current location.
- **Lines 19-20**: 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 21-22**: 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 persistent 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).
## Summary
In this lesson we learned how to create our own Command, add it to a CmdSet and then to ourselves.
We also upset a dragon.
In the next lesson we'll learn how to hit Smaug with different weapons. We'll also
get into how we replace and extend Evennia's default Commands.

View file

@ -1,311 +0,0 @@
# Using commands and building stuff
In this lesson we will test out what we can do in-game out-of-the-box. Evennia ships with
[around 90 default commands](../../../Components/Default-Commands.md), and while you can override those as you please,
they can be quite useful.
Connect and log into your new game and you will end up in the "Limbo" location. This
is the only room in the game at this point. Let's explore the commands a little.
The default commands has syntax [similar to MUX](../../../Concepts/Using-MUX-as-a-Standard.md):
command[/switch/switch...] [arguments ...]
An example would be
create/drop box
A _/switch_ is a special, optional flag to the command to make it behave differently. It is always
put directly after the command name, and begins with a forward slash (`/`). The _arguments_ are one
or more inputs to the commands. It's common to use an equal sign (`=`) when assigning something to
an object.
> Are you used to commands starting with @, like @create? That will work too. Evennia simply ignores
> the preceeding @.
## Getting help
help
Will give you a list of all commands available to you. Use
help <commandname>
to see the in-game help for that command.
## Looking around
The most common comman is
look
This will show you the description of the current location. `l` is an alias.
When targeting objects in commands you have two special labels you can use, `here` for the current
room or `me`/`self` to point back to yourself. So
look me
will give you your own description. `look here` is, in this case, the same as plain `look`.
## Stepping Down From Godhood
If you just installed Evennia, your very first player account is called user #1, also known as the
_superuser_ or _god user_. This user is very powerful, so powerful that it will override many game
restrictions such as locks. This can be useful, but it also hides some functionality that you might
want to test.
To temporarily step down from your superuser position you can use the `quell` command in-game:
quell
This will make you start using the permission of your current character's level instead of your
superuser level. If you didn't change any settings your game Character should have an _Developer_
level permission - high as can be without bypassing locks like the superuser does. This will work
fine for the examples on this page. Use
unquell
to get superuser status again when you are done.
## Creating an Object
Basic objects can be anything -- swords, flowers and non-player characters. They are created using
the `create` command:
create box
This created a new 'box' (of the default object type) in your inventory. Use the command `inventory`
(or `i`) to see it. Now, 'box' is a rather short name, let's rename it and tack on a few aliases.
name box = very large box;box;very;crate
```{warning} MUD clients and semi-colon
Some traditional MUD clients use the semi-colon `;` to separate client inputs. If so,
the above line will give an error. You need to change your client to use another command-separator
or to put it in 'verbatim' mode. If you still have trouble, use the Evennia web client instead.
```
We now renamed the box to _very large box_ (and this is what we will see when looking at it), but we
will also recognize it by any of the other names we give - like _crate_ or simply _box_ as before.
We could have given these aliases directly after the name in the `create` command, this is true for
all creation commands - you can always tag on a list of `;`-separated aliases to the name of your
new object. If you had wanted to not change the name itself, but to only add aliases, you could have
used the `alias` command.
We are currently carrying the box. Let's drop it (there is also a short cut to create and drop in
one go by using the `/drop` switch, for example `create/drop box`).
drop box
Hey presto - there it is on the ground, in all its normality.
examine box
This will show some technical details about the box object. For now we will ignore what this
information means.
Try to `look` at the box to see the (default) description.
look box
You see nothing special.
The description you get is not very exciting. Let's add some flavor.
desc box = This is a large and very heavy box.
If you try the `get` command we will pick up the box. So far so good, but if we really want this to
be a large and heavy box, people should _not_ be able to run off with it that easily. To prevent
this we need to lock it down. This is done by assigning a _Lock_ to it. Make sure the box was
dropped in the room, then try this:
lock box = get:false()
Locks represent a rather [big topic](../../../Components/Locks.md), but for now that will do what we want. This will lock
the box so noone can lift it. The exception is superusers, they override all locks and will pick it
up anyway. Make sure you are quelling your superuser powers and try to get the box now:
> get box
You can't get that.
Think thís default error message looks dull? The `get` command looks for an [Attribute](../../../Components/Attributes.md)
named `get_err_msg` for returning a nicer error message (we just happen to know this, you would need
to peek into the
[code](https://github.com/evennia/evennia/blob/master/evennia/commands/default/general.py#L235) for
the `get` command to find out.). You set attributes using the `set` command:
set box/get_err_msg = It's way too heavy for you to lift.
Try to get it now and you should see a nicer error message echoed back to you. To see what this
message string is in the future, you can use 'examine.'
examine box/get_err_msg
Examine will return the value of attributes, including color codes. `examine here/desc` would return
the raw description of your current room (including color codes), so that you can copy-and-paste to
set its description to something else.
You create new Commands (or modify existing ones) in Python outside the game. We will get to that
later, in the [Commands tutorial](./Beginner-Tutorial-Adding-Commands.md).
## Get a Personality
[Scripts](../../../Components/Scripts.md) are powerful out-of-character objects useful for many "under the hood" things.
One of their optional abilities is to do things on a timer. To try out a first script, let's put one
on ourselves. There is an example script in `evennia/contrib/tutorials/bodyfunctions/bodyfunctions.py`
that is called `BodyFunctions`. To add this to us we will use the `script` command:
script self = tutorials.bodyfunctions.BodyFunctions
This string will tell Evennia to dig up the Python code at the place we indicate. It already knows
to look in the `contrib/` folder, so we don't have to give the full path.
> Note also how we use `.` instead of `/` (or `\` on Windows). This is a so-called "Python path". In a Python-path,
> you separate the parts of the path with `.` and skip the `.py` file-ending. Importantly, it also allows you to point to
Python code _inside_ files, like the `BodyFunctions` class inside `bodyfunctions.py` (we'll get to classes later).
These "Python-paths" are used extensively throughout Evennia.
Wait a while and you will notice yourself starting making random observations ...
script self
This will show details about scripts on yourself (also `examine` works). You will see how long it is
until it "fires" next. Don't be alarmed if nothing happens when the countdown reaches zero - this
particular script has a randomizer to determine if it will say something or not. So you will not see
output every time it fires.
When you are tired of your character's "insights", kill the script with
script/stop self = tutorials.bodyfunctions.BodyFunctions
You create your own scripts in Python, outside the game; the path you give to `script` is literally
the Python path to your script file. The [Scripts](../../../Components/Scripts.md) page explains more details.
## Pushing Your Buttons
If we get back to the box we made, there is only so much fun you can have with it at this point. It's
just a dumb generic object. If you renamed it to `stone` and changed its description, noone would be
the wiser. However, with the combined use of custom [Typeclasses](../../../Components/Typeclasses.md), [Scripts](../../../Components/Scripts.md)
and object-based [Commands](../../../Components/Commands.md), you could expand it and other items to be as unique, complex
and interactive as you want.
Let's take an example. So far we have only created objects that use the default object typeclass
named simply `Object`. Let's create an object that is a little more interesting. Under
`evennia/contrib/tutorial_examples` there is a module `red_button.py`. It contains the enigmatic
`RedButton` class.
Let's make us one of _those_!
create/drop button:tutorials.red_button.RedButton
The same way we did with the Script Earler, we specify a "Python-path" to the Python code we want Evennia
to use for creating the object. There you go - one red button.
The RedButton is an example object intended to show off a few of Evennia's features. You will find
that the [Typeclass](../../../Components/Typeclasses.md) and [Commands](../../../Components/Commands.md) controlling it are
inside [evennia/contrib/tutorials/red_button](../../../api/evennia.contrib.tutorials.red_button.md)
If you wait for a while (make sure you dropped it!) the button will blink invitingly.
Why don't you try to push it ...?
Surely a big red button is meant to be pushed.
You know you want to.
```{warning} Don't press the invitingly blinking red button.
```
## Making Yourself a House
The main command for shaping the game world is `dig`. For example, if you are standing in Limbo you
can dig a route to your new house location like this:
dig house = large red door;door;in,to the outside;out
This will create a new room named 'house'. Spaces at the start/end of names and aliases are ignored
so you could put more air if you wanted. This call will directly create an exit from your current
location named 'large red door' and a corresponding exit named 'to the outside' in the house room
leading back to Limbo. We also define a few aliases to those exits, so people don't have to write
the full thing all the time.
If you wanted to use normal compass directions (north, west, southwest etc), you could do that with
`dig` too. But Evennia also has a limited version of `dig` that helps for compass directions (and
also up/down and in/out). It's called `tunnel`:
tunnel sw = cliff
This will create a new room "cliff" with an exit "southwest" leading there and a path "northeast"
leading back from the cliff to your current location.
You can create new exits from where you are, using the `open` command:
open north;n = house
This opens an exit `north` (with an alias `n`) to the previously created room `house`.
If you have many rooms named `house` you will get a list of matches and have to select which one you
want to link to.
Follow the north exit to your 'house' or `teleport` to it:
north
or:
teleport house
To manually open an exit back to Limbo (if you didn't do so with the `dig` command):
open door = limbo
(You can also us the #dbref of limbo, which you can find by using `examine here` when in limbo).
## Reshuffling the World
You can find things using the `find` command. Assuming you are back at `Limbo`, let's teleport the
_large box_ to our house.
teleport box = house
very large box is leaving Limbo, heading for house.
Teleported very large box -> house.
We can still find the box by using find:
find box
One Match(#1-#8):
very large box(#8) - src.objects.objects.Object
Knowing the `#dbref` of the box (#8 in this example), you can grab the box and get it back here
without actually yourself going to `house` first:
teleport #8 = here
As mentioned, `here` is an alias for 'your current location'. The box should now be back in Limbo with you.
We are getting tired of the box. Let's destroy it.
destroy box
It will ask you for confirmation. Once you give it, the box will be gone.
You can destroy many objects in one go by giving a comma-separated list of objects (or a range
of #dbrefs, if they are not in the same location) to the command.
## Adding a Help Entry
The Command-help is something you modify in Python code. We'll get to that when we get to how to
add Commands. But you can also add regular help entries, for example to explain something about
the history of your game world:
sethelp History = At the dawn of time ...
You will now find your new `History` entry in the `help` list and read your help-text with `help History`.
## Adding a World
After this brief introduction to building and using in-game commands you may be ready to see a more fleshed-out
example. Evennia comes with a tutorial world for you to explore. We will try that out in the next lesson.

View file

@ -1,48 +0,0 @@
# Creating things
We have already created some things - dragons for example. There are many different things to create
in Evennia though. In the last lesson we learned about typeclasses, the way to make objects persistent in the database.
Given the path to a Typeclass, there are three ways to create an instance of it:
- Firstly, you can call the class directly, and then `.save()` it:
obj = SomeTypeClass(db_key=...)
obj.save()
This has the drawback of being two operations; you must also import the class and have to pass
the actual database field names, such as `db_key` instead of `key` as keyword arguments.
- Secondly you can use the Evennia creation helpers:
obj = evennia.create_object(SomeTypeClass, key=...)
This is the recommended way if you are trying to create things in Python. The first argument can either be
the class _or_ the python-path to the typeclass, like `"path.to.SomeTypeClass"`. It can also be `None` in which
case the Evennia default will be used. While all the creation methods
are available on `evennia`, they are actually implemented in [evennia/utils/create.py](../../../api/evennia.utils.create.md).
- Finally, you can create objects using an in-game command, such as
create/drop obj:path.to.SomeTypeClass
As a developer you are usually best off using the two other methods, but a command is usually the only way
to let regular players or builders without Python-access help build the game world.
## Creating Objects
This is one of the most common creation-types. These are entities that inherits from `DefaultObject` at any distance.
They have an existence in the game world and includes rooms, characters, exits, weapons, flower pots and castles.
> py
> import evennia
> rose = evennia.create_object(key="rose")
Since we didn't specify the `typeclass` as the first argument, the default given by `settings.BASE_OBJECT_TYPECLASS`
(`typeclasses.objects.Object`) will be used.
## Creating Accounts
An _Account_ is an out-of-character (OOC) entity, with no existence in the game world.
You can find the parent class for Accounts in `typeclasses/accounts.py`.
_TODO_

View file

@ -1,432 +0,0 @@
# Advanced searching - Django Database queries
```{important} More advanced lesson!
Learning about Django's query language is very useful once you start doing more
advanced things in Evennia. But it's not strictly needed out the box and can be
a little overwhelming for a first reading. So if you are new to Python and
Evennia, feel free to just skim this lesson and refer back to it later when
you've gained more experience.
```
The search functions and methods we used in the previous lesson are enough for most cases.
But sometimes you need to be more specific:
- You want to find all `Characters` ...
- ... who are in Rooms tagged as `moonlit` ...
- ... _and_ who has the Attribute `lycantrophy` with a level higher than 2 ...
- ... because they should immediately transform to werewolves!
In principle you could achieve this with the existing search functions combined with a lot of loops
and if statements. But for something non-standard like this, querying the database directly will be
much more efficient.
Evennia uses [Django](https://www.djangoproject.com/) to handle its connection to the database.
A [django queryset](https://docs.djangoproject.com/en/3.0/ref/models/querysets/) represents
a database query. One can add querysets together to build ever-more complicated queries. Only when
you are trying to use the results of the queryset will it actually call the database.
The normal way to build a queryset is to define what class of entity you want to search by getting its
`.objects` resource, and then call various methods on that. We've seen this one before:
all_weapons = Weapon.objects.all()
This is now a queryset representing all instances of `Weapon`. If `Weapon` had a subclass `Cannon` and we
only wanted the cannons, we would do
all_cannons = Cannon.objects.all()
Note that `Weapon` and `Cannon` are _different_ typeclasses. This means that you
won't find any `Weapon`-typeclassed results in `all_cannons`. Vice-versa, you
won't find any `Cannon`-typeclassed results in `all_weapons`. This may not be
what you expect.
If you want to get all entities with typeclass `Weapon` _as well_ as all the
subclasses of `Weapon`, such as `Cannon`, you need to use the `_family` type of
query:
```{sidebar} _family
The all_family, filter_family etc is an Evennia-specific
thing. It's not part of regular Django.
```
really_all_weapons = Weapon.objects.all_family()
This result now contains both `Weapon` and `Cannon` instances (and any other
entities whose typeclasses inherit at any distance from `Weapon`, like `Musket` or
`Sword`).
To limit your search by other criteria than the Typeclass you need to use `.filter`
(or `.filter_family`) instead:
roses = Flower.objects.filter(db_key="rose")
This is a queryset representing all flowers having a `db_key` equal to `"rose"`.
Since this is a queryset you can keep adding to it; this will act as an `AND` condition.
local_roses = roses.filter(db_location=myroom)
We could also have written this in one statement:
local_roses = Flower.objects.filter(db_key="rose", db_location=myroom)
We can also `.exclude` something from results
local_non_red_roses = local_roses.exclude(db_key="red_rose")
It's important to note that we haven't called the database yet! Not until we
actually try to examine the result will the database be called. Here the
database is called when we try to loop over it (because now we need to actually
get results out of it to be able to loop):
for rose in local_non_red_roses:
print(rose)
From now on, the queryset is _evaluated_ and we can't keep adding more queries to it - we'd need to
create a new queryset if we wanted to find some other result. Other ways to evaluate the queryset is to
print it, convert it to a list with `list()` and otherwise try to access its results.
Note how we use `db_key` and `db_location`. This is the actual names of these
database fields. By convention Evennia uses `db_` in front of every database
field. When you use the normal Evennia search helpers and objects you can skip
the `db_` but here we are calling the database directly and need to use the
'real' names.
```{sidebar} database fields
Each database table have only a few fields. For `Objects`, the most common ones
are `db_key`, `db_location` and `db_destination`. When accessing them they are
normally accessed just as `obj.key`, `obj.location` and `obj.destination`. You
only need to remember the `db_` when using them in database queries. The object
description, `obj.db.desc` is not such a hard-coded field, but one of many
arbitrary Attributes attached to the Object.
```
Here are the most commonly used methods to use with the `objects` managers:
- `filter` - query for a listing of objects based on search criteria. Gives empty queryset if none
were found.
- `get` - query for a single match - raises exception if none were found, or more than one was
found.
- `all` - get all instances of the particular type.
- `filter_family` - like `filter`, but search all sub classes as well.
- `get_family` - like `get`, but search all sub classes as well.
- `all_family` - like `all`, but return entities of all subclasses as well.
> All of Evennia search functions use querysets under the hood. The `evennia.search_*` functions actually
> return querysets, which means you could in principle keep adding queries to their results as well.
## Queryset field lookups
Above we found roses with exactly the `db_key` `"rose"`. This is an _exact_ match that is _case sensitive_,
so it would not find `"Rose"`.
# this is case-sensitive and the same as =
roses = Flower.objects.filter(db_key__exact="rose"
# the i means it's case-insensitive
roses = Flower.objects.filter(db_key__iexact="rose")
The Django field query language uses `__` similarly to how Python uses `.` to access resources. This
is because `.` is not allowed in a function keyword.
roses = Flower.objects.filter(db_key__icontains="rose")
This will find all flowers whose name contains the string `"rose"`, like `"roses"`, `"wild rose"` etc. The
`i` in the beginning makes the search case-insensitive. Other useful variations to use
are `__istartswith` and `__iendswith`. You can also use `__gt`, `__ge` for "greater-than"/"greater-or-equal-than"
comparisons (same for `__lt` and `__le`). There is also `__in`:
swords = Weapons.objects.filter(db_key__in=("rapier", "two-hander", "shortsword"))
One also uses `__` to access foreign objects like Tags. Let's for example assume
this is how we have identified mages:
char.tags.add("mage", category="profession")
Now, in this case we have an Evennia helper to do this search:
mages = evennia.search_tags("mage", category="profession")
But this will find all Objects with this tag+category. Maybe you are only looking for Vampire mages:
sparkly_mages = Vampire.objects.filter(db_tags__db_key="mage", db_tags__db_category="profession")
This looks at the `db_tags` field on the `Vampire` and filters on the values of each tag's
`db_key` and `db_category` together.
For more field lookups, see the
[django docs](https://docs.djangoproject.com/en/3.0/ref/models/querysets/#field-lookups) on the subject.
## Get that werewolf ...
Let's see if we can make a query for the werewolves in the moonlight we mentioned at the beginning
of this lesson.
Firstly, we make ourselves and our current location match the criteria, so we can test:
> py here.tags.add("moonlit")
> py me.db.lycantrophy = 3
This is an example of a more complex query. We'll consider it an example of what is
possible.
```{sidebar} Line breaks
Note the way of writing this code. It would have been very hard to read if we
just wrote it in one long line. But since we wrapped it in `(...)` we can spread
it out over multiple lines without worrying about line breaks!
```
```python
from typeclasses.characters import Character
will_transform = (
Character.objects
.filter(
db_location__db_tags__db_key__iexact="moonlit",
db_attributes__db_key="lycantrophy",
db_attributes__db_value__gt=2
)
)
```
- We want to find `Character`s, so we access `.objects` on the `Character` typeclass.
- We start to filter ...
-
- ... by accessing the `db_location` field (usually this is a Room)
- ... and on that location, we get the value of `db_tags` (this is a _many-to-many_ database field
that we can treat like an object for this purpose; it references all Tags on the location)
- ... and from those `Tags`, we looking for `Tags` whose `db_key` is "monlit" (non-case sensitive).
- ... We also want only Characters with `Attributes` whose `db_key` is exactly `"lycantrophy"`
- ... at the same time as the `Attribute`'s `db_value` is greater-than 2.
Running this query makes our newly lycantrophic Character appear in `will_transform` so we
know to transform it. Success!
> Don't confuse database fields with [Attributes](../../../Components/Attributes.md) you set via `obj.db.attr = 'foo'` or
`obj.attributes.add()`. Attributes are custom database entities *linked* to an object. They are not
separate fields *on* that object like `db_key` or `db_location` are.
## Complex queries
All examples so far used `AND` relations. The arguments to `.filter` are added together with `AND`
("we want tag room to be "monlit" _and_ lycantrhopy be > 2").
For queries using `OR` and `NOT` we need Django's
[Q object](https://docs.djangoproject.com/en/1.11/topics/db/queries/#complex-lookups-with-q-objects). It is
imported from Django directly:
from django.db.models import Q
The `Q` is an object that is created with the same arguments as `.filter`, for example
Q(db_key="foo")
You can then use this `Q` instance as argument in a `filter`:
q1 = Q(db_key="foo")
Character.objects.filter(q1)
The useful thing about `Q` is that these objects can be chained together with special symbols (bit operators):
`|` for `OR` and `&` for `AND`. A tilde `~` in front negates the expression inside the `Q` and thus
works like `NOT`.
q1 = Q(db_key="Dalton")
q2 = Q(db_location=prison)
Character.objects.filter(q1 | ~q2)
Would get all Characters that are either named "Dalton" _or_ which is _not_ in prison. The result is a mix
of Daltons and non-prisoners.
Let us expand our original werewolf query. Not only do we want to find all
Characters in a moonlit room with a certain level of `lycanthrophy`. Now we also
want the full moon to immediately transform people who were recently bitten,
even if their `lycantrophy` level is not yet high enough (more dramatic this
way!). When you get bitten, you'll get a Tag `recently_bitten` put on you to
indicate this.
This is how we'd change our query:
```python
from django.db.models import Q
will_transform = (
Character.objects
.filter(
Q(db_location__db_tags__db_key__iexact="moonlit")
& (
Q(db_attributes__db_key="lycantrophy",
db_attributes__db_value__gt=2)
| Q(db_tags__db_key__iexact="recently_bitten")
))
.distinct()
)
```
That's quite compact. It may be easier to see what's going on if written this way:
```python
from django.db.models import Q
q_moonlit = Q(db_location__db_tags__db_key__iexact="moonlit")
q_lycantropic = Q(db_attributes__db_key="lycantrophy", db_attributes__db_value__gt=2)
q_recently_bitten = Q(db_tags__db_key__iexact="recently_bitten")
will_transform = (
Character.objects
.filter(q_moonlit & (q_lycantropic | q_recently_bitten))
.distinct()
)
```
```{sidebar} SQL
These Python structures are internally converted to SQL, the native language of
the database. If you are familiar with SQL, these are many-to-many tables
joined with `LEFT OUTER JOIN`, which may lead to multiple merged rows combining
the same object with different relations.
```
This reads as "Find all Characters in a moonlit room that either has the
Attribute `lycantrophy` higher than two, _or_ which has the Tag
`recently_bitten`". With an OR-query like this it's possible to find the same
Character via different paths, so we add `.distinct()` at the end. This makes
sure that there is only one instance of each Character in the result.
## Annotations
What if we wanted to filter on some condition that isn't represented easily by a
field on the object? Maybe we want to find rooms only containing five or more
objects?
We *could* do it like this (don't actually do it this way!):
```python
from typeclasses.rooms import Room
all_rooms = Rooms.objects.all()
rooms_with_five_objects = []
for room in all_rooms:
if len(room.contents) >= 5:
rooms_with_five_objects.append(room)
```
Above we get all rooms and then use `list.append()` to keep adding the right
rooms to an ever-growing list. This is _not_ a good idea, once your database
grows this will be unnecessarily computing-intensive. The database is much more
suitable for this.
_Annotations_ allow you to set a 'variable' inside the query that you can then
access from other parts of the query. Let's do the same example as before
directly in the database:
```python
from typeclasses.rooms import Room
from django.db.models import Count
rooms = (
Room.objects
.annotate(
num_objects=Count('locations_set'))
.filter(num_objects__gte=5)
)
```
`Count` is a Django class for counting the number of things in the database.
Here we first create an annotation `num_objects` of type `Count`. It creates an in-database function
that will count the number of results inside the database.
> Note the use of `location_set` in that `Count`. The `*_set` is a back-reference automatically created by
Django. In this case it allows you to find all objects that *has the current object as location*.
Next we filter on this annotation, using the name `num_objects` as something we
can filter for. We use `num_objects__gte=5` which means that `num_objects`
should be greater than or equal to 5. This is a little harder to get one's head
around but much more efficient than lopping over all objects in Python.
## F-objects
What if we wanted to compare two dynamic parameters against one another in a
query? For example, what if instead of having 5 or more objects, we only wanted
objects that had a bigger inventory than they had tags (silly example, but ...)?
This can be with Django's [F objects](https://docs.djangoproject.com/en/1.11/ref/models/expressions/#f-expressions).
So-called F expressions allow you to do a query that looks at a value of each
object in the database.
```python
from django.db.models import Count, F
from typeclasses.rooms import Room
result = (
Room.objects
.annotate(
num_objects=Count('locations_set'),
num_tags=Count('db_tags'))
.filter(num_objects__gt=F('num_tags'))
)
```
Here we used `.annotate` to create two in-query 'variables' `num_objects` and `num_tags`. We then
directly use these results in the filter. Using `F()` allows for also the right-hand-side of the filter
condition to be calculated on the fly, completely within the database.
## Grouping and returning only certain properties
Suppose you used tags to mark someone belonging to an organization. Now you want to make a list and
need to get the membership count of every organization all at once.
The `.annotate`, `.values_list`, and `.order_by` queryset methods are useful for this. Normally when
you run a `.filter`, what you get back is a bunch of full typeclass instances, like roses or swords.
Using `.values_list` you can instead choose to only get back certain properties on objects.
The `.order_by` method finally allows for sorting the results according to some criterion:
```python
from django.db.models import Count
from typeclasses.rooms import Room
result = (
Character.objects
.filter(db_tags__db_category="organization")
.annotate(tagcount=Count('id'))
.order_by('-tagcount'))
.values_list('db_tags__db_key', "tagcount")
```
Here we fetch all Characters who ...
- ... has a tag of category "organization" on them
- ... along the way we count how many different Characters (each `id` is unique) we find for each organization
and store it in a 'variable' `tagcount` using `.annotate` and `Count`
- ... we use this count to sort the result in descending order of `tagcount` (descending because there is a minus sign,
default is increasing order but we want the most popular organization to be first).
- ... and finally we make sure to only return exactly the properties we want, namely the name of the organization tag
and how many matches we found for that organization.
The result queryset will be a list of tuples ordered in descending order by the number of matches,
in a format like the following:
```
[
('Griatch's poets society', 3872),
("Chainsol's Ainneve Testers", 2076),
("Blaufeuer's Whitespace Fixers", 1903),
("Volund's Bikeshed Design Crew", 1764),
("Tehom's Glorious Misanthropes", 1763)
]
```
## Conclusions
We have covered a lot of ground in this lesson and covered several more complex
topics. Knowing how to query using Django is a powerful skill to have.
This concludes the first part of the Evennia starting tutorial - "What we have".
Now we have a good foundation to understand how to plan what our tutorial game
will be about.

View file

@ -1,116 +0,0 @@
# Overview of the Evennia library
```{sidebar} API
API stands for `Application Programming Interface`, a description for how to access the resources of a program or library.
```
A good place to start exploring Evennia is the [Evenia-API frontpage](../../../Evennia-API.md).
This page sums up the main components of Evennia with a short description of each. Try clicking through
to a few entries - once you get deep enough you'll see full descriptions
of each component along with their documentation. You can also click `[source]` to see the full Python source
for each thing.
You can also browse [the evennia repository on github](https://github.com/evennia/evennia). This is exactly
what you can download from us. The github repo is also searchable.
Finally, you can clone the evennia repo to your own computer and read the sources locally. This is necessary
if you want to help with Evennia's development itself. See the
[extended install instructions](../../../Setup/Installation-Git.md) if you want to do this.
## Where is it?
If Evennia is installed, you can import from it simply with
import evennia
from evennia import some_module
from evennia.some_module.other_module import SomeClass
and so on.
If you installed Evennia with `pip install`, the library folder will be installed deep inside your Python
installation. If you cloned the repo there will be a folder `evennia` on your hard drive there.
If you cloned the repo or read the code on `github` you'll find this being the outermost structure:
evennia/
bin/
CHANGELOG.md
...
...
docs/
evennia/
This outer layer is for Evennia's installation and package distribution. That internal folder `evennia/evennia/` is
the _actual_ library, the thing covered by the API auto-docs and what you get when you do `import evennia`.
> The `evennia/docs/` folder contains the sources for this documentation. See
> [contributing to the docs](../../../Contributing-Docs.md) if you want to learn more about how this works.
This the the structure of the Evennia library:
- evennia
- [`__init__.py`](../../../Evennia-API.md#shortcuts) - The "flat API" of Evennia resides here.
- [`settings_default.py`](../../../Setup/Settings.md#settings-file) - Root settings of Evennia. Copy settings
from here to `mygame/server/settings.py` file.
- [`commands/`](../../../Components/Commands.md) - The command parser and handler.
- `default/` - The [default commands](../../../Components/Default-Commands.md) and cmdsets.
- [`comms/`](../../../Components/Channels.md) - Systems for communicating in-game.
- `contrib/` - Optional plugins too game-specific for core Evennia.
- `game_template/` - Copied to become the "game directory" when using `evennia --init`.
- [`help/`](../../../Components/Help-System.md) - Handles the storage and creation of help entries.
- `locale/` - Language files ([i18n](../../../Concepts/Internationalization.md)).
- [`locks/`](../../../Components/Locks.md) - Lock system for restricting access to in-game entities.
- [`objects/`](../../../Components/Objects.md) - In-game entities (all types of items and Characters).
- [`prototypes/`](../../../Components/Prototypes.md) - Object Prototype/spawning system and OLC menu
- [`accounts/`](../../../Components/Accounts.md) - Out-of-game Session-controlled entities (accounts, bots etc)
- [`scripts/`](../../../Components/Scripts.md) - Out-of-game entities equivalence to Objects, also with timer support.
- [`server/`](../../../Components/Portal-And-Server.md) - Core server code and Session handling.
- `portal/` - Portal proxy and connection protocols.
- [`typeclasses/`](../../../Components/Typeclasses.md) - Abstract classes for the typeclass storage and database system.
- [`utils/`](../../../Components/Coding-Utils.md) - Various miscellaneous useful coding resources.
- [`web/`](../../../Concepts/Web-Features.md) - Web resources and webserver. Partly copied into game directory on initialization.
```{sidebar} __init__.py
The `__init__.py` file is a special Python filename used to represent a Python 'package'. When you import `evennia` on its own, you import this file. When you do `evennia.foo` Python will first look for a property `.foo` in `__init__.py` and then for a module or folder of that name in the same location.
```
While all the actual Evennia code is found in the various folders, the `__init__.py` represents the entire
package `evennia`. It contains "shortcuts" to code that is actually located elsewhere. Most of these shortcuts
are listed if you [scroll down a bit](../../../Evennia-API.md) on the Evennia-API page.
## An example of exploring the library
In the previous lesson we took a brief look at `mygame/typeclasses/objects` as an example of a Python module. Let's
open it again. Inside is the `Object` class, which inherits from `DefaultObject`.
Near the top of the module is this line:
from evennia import DefaultObject
We want to figure out just what this DefaultObject offers. Since this is imported directly from `evennia`, we
are actually importing from `evennia/__init__.py`.
[Look at Line 159](github:evennia/__init__.py#159) of `evennia/__init__.py` and you'll find this line:
from .objects.objects import DefaultObject
```{sidebar} Relative and absolute imports
The first full-stop in `from .objects.objects ...` means that we are importing from the current location. This is called a `relative import`. By comparison, `from evennia.objects.objects` is an `absolute import`. In this particular case, the two would give the same result.
```
> You can also look at [the right section of the API frontpage](../../../Evennia-API.md#typeclasses) and click through
> to the code that way.
The fact that `DefaultObject` is imported into `__init__.py` here is what makes it possible to also import
it as `from evennia import DefaultObject` even though the code for the class is not actually here.
So to find the code for `DefaultObject` we need to look in `evennia/objects/objects.py`. Here's how
to look it up in the docs:
1. Open the [API frontpage](../../../Evennia-API.md)
2. Locate the link to [evennia.objects.objects](../../../api/evennia.objects.objects.md) and click on it.
3 You are now in the python module. Scroll down (or search in your web browser) to find the `DefaultObject` class.
4 You can now read what this does and what methods are on it. If you want to see the full source, click the
\[source\] link next to it.

View file

@ -1,199 +0,0 @@
# Overview of your new Game Dir
Next we will take a little detour to look at the _Tutorial World_. This is a little solo adventure
that comes with Evennia, a showcase for some of the things that are possible.
Now we have 'run the game' a bit and started with our forays into Python from inside Evennia.
It is time to start to look at how things look 'outside of the game'. Let's do a tour of your game-dir
Like everywhere in the docs we'll assume it's called `mygame`.
> When looking through files, ignore files ending with `.pyc` and the
`__pycache__` folder if it exists. This is internal Python compilation files that you should never
> need to touch. Files `__init__.py` is also often empty and can be ignored (they have to do with
> Python package management).
You may have noticed when we were building things in-game that we would often refer to code through
"python paths", such as
```{sidebar} Python-paths
A 'python path' uses '.' instead of '/' or '`\\`' and skips the `.py` ending of files. It can also point to the code contents of python files. Since Evennia is already looking for code in your game dir, your python paths can start from there. So a path `/home/foo/devel/mygame/commands/command.py` would translate to a Python-path `commands.command`.
```
create/drop button:tutorial_examples.red_button.RedButton
This is a fundamental aspect of coding Evennia - _you create code and then you tell Evennia where that
code is and when it should be used_. Above we told it to create a red button by pulling from specific code
in the `contribs/` folder but the same principle is true everywhere. So it's important to know where code is
and how you point to it correctly.
- `mygame/`
- `commands/` - This holds all your custom commands (user-input handlers). You both add your own
and override Evennia's defaults from here.
- `server`/ - The structure of this folder should not change since Evennia expects it.
- `conf/` - All server configuration files sits here. The most important file is `settings.py`.
- `logs/` - Server log files are stored here. When you use `evennia --log` you are actually
tailing the files in this directory.
- `typeclasses/` - this holds empty templates describing all database-bound entities in the
game, like Characters, Scripts, Accounts etc. Adding code here allows to customize and extend
the defaults.
- `web/` - This is where you override and extend the default templates, views and static files used
for Evennia's web-presence, like the website and the HTML5 webclient.
- `world/` - this is a "miscellaneous" folder holding everything related to the world you are
building, such as build scripts and rules modules that don't fit with one of the other folders.
> The `server/` subfolder should remain the way it is - Evennia expects this. But you could in
> principle change the structure of the rest of your game dir as best fits your preference.
> Maybe you don't need a world/ folder but prefer many folders with different aspects of your world?
> Or a new folder 'rules' for your RPG rules? This is fine. If you move things around you just need
> to update Evennia's default settings to point to the right places in the new structure.
## commands/
The `commands/` folder holds Python modules related to creating and extending the [Commands](../../../Components/Commands.md)
of Evennia. These manifest in game like the server understanding input like `look` or `dig`.
```{sidebar} Classes
A `class` is template for creating object-instances of a particular type in Python. We will explain classes in more detail in the next lesson.
```
- [command.py](github:evennia/game_template/commands/command.py) (Python-path: `commands.command`) - this contain the
base _classes_ for designing new input commands, or override the defaults.
- [default_cmdsets.py](github:evennia/game_template/commands/default_cmdsets.py) (Python path: `commands.default_commands`) -
a cmdset (Command-Set) groups Commands together. Command-sets can be added and removed from objects on the fly,
meaning a user could have a different set of commands (or versions of commands) available depending on their circumstance
in the game. In order to add a new command to the game, it's common to import the new command-class
from `command.py` and add it to one of the default cmdsets in this module.
## server/
This folder contains resource necessary for running Evennia. Contrary to the other folders, the structure
of this should be kept the way it is.
- `evennia.db3` - you will only have this file if you are using the default SQLite3 database. This file
contains the entire database. Just copy it to make a backup. For development you could also just
make a copy once you have set up everything you need and just copy that back to 'reset' the state.
If you delete this file you can easily recreate it by running `evennia migrate`.
### server/logs/
This holds the server logs. When you do `evennia --log`, the evennia program is in fact tailing and concatenating
the `server.log` and `portal.log` files in this directory. The logs are rotated every week. Depending on your settings,
other logs, like the webserver HTTP request log can also be found here.
### server/conf/
This contains all configuration files of the Evennia server. These are regular Python modules which
means that they must be extended with valid Python. You can also add logic to them if you wanted to.
Common for the settings is that you generally will never them directly via their python-path; instead Evennia
knows where they are and will read them to configure itself at startup.
- `settings.py` - this is by far the most important file. It's nearly empty by default, rather you
are expected to copy&paste the changes you need from [evennia/default_settings.py](github:evennia/default_settings.py).
The default settings file is extensively documented. Importing/accessing the values in the settings
file is done in a special way, like this:
from django.conf import settings
To get to the setting `TELNET_PORT` in the settings file you'd then do
telnet_port = settings.TELNET_PORT
You cannot assign to the settings file dynamically; you must change the `settings.py` file directly to
change a setting.
- `secret_settings.py` - If you are making your code effort public, you may not want to share all settings online.
There may be server-specific secrets or just fine-tuning for your game systems that you prefer be kept secret
from the players. Put such settings in here, it will override values in `settings.py` and not be included in
version control.
- `at_initial_setup.py` - When Evennia starts up for the very first time, it does some basic tasks, like creating the
superuser and Limbo room. Adding to this file allows to add more actions for it to for first-startup.
- `at_search.py` - When searching for objects and either finding no match or more than one match, it will
respond by giving a warning or offering the user to differentiate between the multiple matches. Modifying
the code here will change this behavior to your liking.
- `at_server_startstop.py` - This allows to inject code to execute every time the server starts, stops or reloads
in different ways.
- `connection_screens.py` - This allows for changing the connection screen you see when you first connect to your
game.
- `inlinefuncs.py` - _Inlinefuncs_ are optional and limited 'functions' that can be embedded in any strings being
sent to a player. They are written as `$funcname(args)` and are used to customize the output
depending on the user receiving it. For example sending people the text `"Let's meet at $realtime(13:00, GMT)!`
would show every player seeing that string the time given in their own time zone. The functions added to this
module will become new inlinefuncs in the game.
- `inputfucs.py` - When a command like `look` is received by the server, it is handled by an _inputfunc_
that redirects it to the cmdhandler system. But there could be other inputs coming from the clients, like
button-presses or the request to update a health-bar. While most common cases are already covered, this is
where one adds new functions to process new types of input.
- `lockfuncs.py` - _Locks_ restrict access to things in-game. Lock funcs are used in a mini-language
to defined more complex locks. For example you could have a lockfunc that checks if the user is carrying
a given item, is bleeding or has a certain skill value. New functions added in this modules will
become available for use in lock definitions.
- `mssp.py` - Mud Server Status Protocol is a way for online MUD archives/listings (which you usually have
to sign up for) to track which MUDs are currently online, how many players they have etc. While Evennia handles
the dynamic information automatically, this is were you set up the meta-info about your game, such as its
theme, if player-killing is allowed and so on. This is a more generic form of the Evennia Game directory.
- `portal_services_plugins.py` - If you want to add new external connection protocols to Evennia, this is the place
to add them.
- `server_services_plugins.py` - This allows to override internal server connection protocols.
- `web_plugins.py` - This allows to add plugins to the Evennia webserver as it starts.
### typeclasses/
The [Typeclasses](../../../Components/Typeclasses.md) of Evennia are Evennia-specific Python classes whose instances save themselves
to the database. This allows a Character to remain in the same place and your updated strength stat to still
be the same after a server reboot.
- [accounts.py](github:evennia/game_template/typeclasses/accounts.py) (Python-path: `typeclasses.accounts`) - An
[Account](../../../Components/Accounts.md) represents the player connecting to the game. It holds information like email,
password and other out-of-character details.
- [channels.py](github:evennia/game_template/typeclasses/channels.py) (Python-path: `typeclasses.channels`) -
[Channels](../../../Components/Channels.md) are used to manage in-game communication between players.
- [objects.py](github:evennia/game_template/typeclasses/objects.py) (Python-path: `typeclasses.objects`) -
[Objects](../../../Components/Objects.md) represent all things having a location within the game world.
- [characters.py](github:evennia/game_template/typeclasses/characters.py) (Python-path: `typeclasses.characters`) -
The [Character](../../../Components/Objects.md#characters) is a subclass of Objects, controlled by Accounts - they are the player's
avatars in the game world.
- [rooms.py](github:evennia/game_template/typeclasses/rooms.py) (Python-path: `typeclasses.rooms`) - A
[Room](../../../Components/Objects.md#rooms) is also a subclass of Object; describing discrete locations. While the traditional
term is 'room', such a location can be anything and on any scale that fits your game, from a forest glade,
an entire planet or an actual dungeon room.
- [exits.py](github:evennia/game_template/typeclasses/exits.py) (Python-path: `typeclasses.exits`) -
[Exits](../../../Components/Objects.md#exits) is another subclass of Object. Exits link one Room to another.
- [scripts.py](github:evennia/game_template/typeclasses/scripts.py) (Python-path: `typeclasses.scripts`) -
[Scripts](../../../Components/Scripts.md) are 'out-of-character' objects. They have no location in-game and can serve as basis for
anything that needs database persistence, such as combat, weather, or economic systems. They also
have the ability to execute code repeatedly, on a timer.
### web/
This folder contains folders for overriding the default web-presence of Evennia with your own designs.
Most of these folders are empty except for a README file or a subset of other empty folders.
- `media/` - this empty folder is where you can place your own images or other media files you want the
web server to serve. If you are releasing your game with a lot of media (especially if you want videos) you
should consider re-pointing Evennia to use some external service to serve your media instead.
- `static_overrides/` - 'static' files include fonts, CSS and JS. Within this folder you'll find sub-folders for
overriding the static files for the `admin` (this is the Django web-admin), the `webclient` (this is thet
HTML5 webclient) and the `website`. Adding files to this folder will replace same-named files in the
default web presence.
- `template_overrides/` - these are HTML files, for the `webclient` and the `website`. HTML files are written
using [Jinja](https://jinja.palletsprojects.com/en/2.11.x/) templating, which means that one can override
only particular parts of a default template without touching others.
- `static/` - this is a work-directory for the web system and should _not_ be manually modified. Basically,
Evennia will copy static data from `static_overrides` here when the server starts.
- `urls.py` - this module links up the Python code to the URLs you go to in the browser.
### world/
This folder only contains some example files. It's meant to hold 'the rest' of your game implementation. Many
people change and re-structure this in various ways to better fit their ideas.
- [batch_cmds.ev](github:evennia/game_template/world/batch_cmds.ev) - This is an `.ev` file, which is essentially
just a list of Evennia commands to execute in sequence. This one is empty and ready to expand on. The
[Tutorial World](./Beginner-Tutorial-Tutorial-World.md) was built with such a batch-file.
- [prototypes.py](github:evennia/game_template/world/prototypes.py) - A [prototype](../../../Components/Prototypes.md) is a way
to easily vary objects without changing their base typeclass. For example, one could use prototypes to
tell that Two goblins, while both of the class 'Goblin' (so they follow the same code logic), should have different
equipment, stats and looks.

View file

@ -1,605 +0,0 @@
# Making objects persistent
Now that we have learned a little about how to find things in the Evennia library, let's use it.
In the [Python classes and objects](./Beginner-Tutorial-Python-classes-and-objects.md) lesson we created the dragons Fluffy, Cuddly
and Smaug and made them fly and breathe fire. So far our dragons are short-lived - whenever we `restart`
the server or `quit()` out of python mode they are gone.
This is what you should have in `mygame/typeclasses/monsters.py` so far:
```python
class Monster:
"""
This is a base class for Monsters.
"""
def __init__(self, key):
self.key = key
def move_around(self):
print(f"{self.key} is moving!")
class Dragon(Monster):
"""
This is a dragon-specific monster.
"""
def move_around(self):
super().move_around()
print("The world trembles.")
def firebreath(self):
"""
Let our dragon breathe fire.
"""
print(f"{self.key} breathes fire!")
```
## Our first persistent object
At this point we should know enough to understand what is happening in `mygame/typeclasses/objects.py`. Let's
open it:
```python
"""
module docstring
"""
from evennia import DefaultObject
class Object(DefaultObject):
"""
class docstring
"""
pass
```
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!
> 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](evennia.objects.objects.DefaultObject). The docstring for
> the `Object` class can also help.
One thing that Evennia classes 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.
Go back to `mygame/typeclasses/monsters.py`. Change it as follows:
```python
from typeclasses.objects import Object
class Monster(Object):
"""
This is a base class for Monsters.
"""
def move_around(self):
print(f"{self.key} is moving!")
class Dragon(Monster):
"""
This is a dragon-specific Monster.
"""
def move_around(self):
super().move_around()
print("The world trembles.")
def firebreath(self):
"""
Let our dragon breathe fire.
"""
print(f"{self.key} breathes fire!")
```
Don't forget to save. We removed `Monster.__init__` and made `Monster` 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!
### Making a new object by calling the class
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.
```
> py
> from typeclasses.monsters import Dragon
> smaug = Dragon(db_key="Smaug", db_location=here)
> smaug.save()
> smaug.move_around()
Smaug is moving!
The world trembles.
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.
> quit()
Python Console is closing.
> look
You should now see that Smaug _is in the room with you_. Woah!
> reload
> look
_He's still there_... What we just did was 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).
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()`:
> py smaug = me.search("Smaug") ; smaug.firebreath()
Smaug breathes fire!
### Creating using create_object
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`:
> py fluffy = evennia.create_object('typeclases.monster.Monster', 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.
If you want to find Fluffy from anywhere, you can use Evennia's `search_object` helper:
> fluffy = evennia.search_object("Fluffy")[0] ; fluffy.move_around()
Fluffy is moving!
> 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]`.
### Creating using create-command
Finally, you can also create a new Dragon using the familiar builder-commands we explored a few lessons ago:
> create/drop Cuddly:typeclasses.monsters.Monster
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
evennia.create_object("typeclasses.monsters.Monster", key="Cuddly", location=here)
That's pretty much all there is to the mighty `create` command! The rest is just parsing for the command
to understand just what the user wants to create.
## Typeclasses
The `Object` (and `DefafultObject` class we inherited from above is what we refer to as a _Typeclass_. This
is an Evennia thing. The instance of a typeclass saves itself to the database when it is created, and after
that you can just search for it to get it back. We use the term _typeclass_ or _typeclassed_ to differentiate
these types of classes and objects from the normal Python classes, whose instances go away on a reload.
The number of typeclasses in Evennia are so few they can be learned by heart:
- `evennia.DefaultObject`: This is the parent of all in-game entities - everything with a location. Evennia makes
a few very useful child classes of this class:
- `evennia.DefaultCharacter`: The default entity represening a player avatar in-game.
- `evennia.DefaultRoom`: A location in the game world.
- `evennia.DefaultExit`: A link between locations.
- `evennia.DefaultAccount`: The OOC representation of a player, holds password and account info.
- `evennia.DefaultChannel`: In-game channels. These could be used for all sorts of in-game communication.
- `evennia.DefaultScript`: Out-of-game objects, with no presence in the game world. Anything you want to create that
needs to be persistent can be stored with these entities, such as combat state, economic systems or what have you.
If you take a look in `mygame/typeclasses/` you'll find modules for each of these. Each contains an empty child
class ready that already inherits from the right parent, ready for you to modify or build from:
- `mygame/typeclasses/objects.py` has `class Object(DefaultObject)`, a class directly inheriting the basic in-game entity, this
works as a base for any object.
- `mygame/typeclasses/characters.py` has `class Character(DefaultCharacter)`
- `mygame/typeclasses/rooms.py` has `class Room(DefaultRoom)`
- `mygame/typeclasses/exits.py` has `class Exit(DefaultExit)`
- `mygame/typeclasses/accounts.py` has `class Account(DefaultAccount)`
- `mygame/typeclasses/channels.py` has `class Channel(DefaultChannel)`
- `mygame/typeclasses/scripts.py` has `class Script(DefaultScript)`
> Notice that the classes in `mygame/typeclasses/` are _not inheriting from each other_. For example,
> `Character` is inheriting from `evennia.DefaultCharacter` and not from `typeclasses.objects.Object`.
> So if you change `Object` you will not cause any change in the `Character` class. If you want that you
> can easily just change the child classes to inherit in that way instead; Evennia doesn't care.
As seen with our `Dragon` example, you don't _have_ to modify these modules directly. You can just make your
own modules and import the base class.
### Examining and defaults
When you do
> create/drop giantess:typeclasses.monsters.Monster
You create a new Monster: giantess.
or
> py evennia.create_object("typeclasses.monsters.Monster", key="Giantess", location=here)
You are specifying exactly which typeclass you want to use to build the Giantess. Let's examine the result:
> examine giantess
-------------------------------------------------------------------------------
Name/key: Giantess (#14)
Typeclass: Monster (typeclasses.monsters.Monster)
Location: Limbo (#2)
Home: Limbo (#2)
Permissions: <None>
Locks: call:true(); control:id(1) or perm(Admin); delete:id(1) or perm(Admin);
drop:holds(); edit:perm(Admin); examine:perm(Builder); get:all();
puppet:pperm(Developer); tell:perm(Admin); view:all()
Persistent attributes:
desc = You see nothing special.
-------------------------------------------------------------------------------
We used the `examine` command briefly in the [lesson about building in-game](./Beginner-Tutorial-Building-Quickstart.md). Now these lines
may be more useful to us:
- **Name/key** - The name of this thing. The value `(#14)` is probably different for you. This is the
unique 'primary key' or _dbref_ for this entity in the database.
- **Typeclass**: This show the typeclass we specified, and the path to it.
- **Location**: We are in Limbo. If you moved elsewhere you'll see that instead. Also the `#dbref` is shown.
- **Permissions**: _Permissions_ are like the inverse to _Locks_ - they are like keys to unlock access to other things.
The giantess have no such keys (maybe fortunately).
- **Locks**: Locks are the inverse of _Permissions_ - specify what criterion _other_ objects must fulfill in order to
access the `giantess` object. This uses a very flexible mini-language. For examine, the line `examine:perm(Builders)`
is read as "Only those with permission _Builder_ or higher can _examine_ this object". Since we are the superuser
we pass (even bypass) such locks with ease.
- **Persistent attributes**: This allows for storing arbitrary, persistent data on the typeclassed entity. We'll get
to those in the next section.
Note how the **Typeclass** line describes exactly where to find the code of this object? This is very useful for
understanding how any object in Evennia works.
What happens if we _don't_ specify the typeclass though?
> create/drop box
You create a new Object: box.
or
> py create.create_object(None, key="box", location=here)
Now check it out:
> examine box
You will find that the **Typeclass** line now reads
Typeclass: Object (typeclasses.objects.Object)
So when you didn't specify a typeclass, Evennia used a default, more specifically the (so far) empty `Object` class in
`mygame/typeclasses/objects.py`. This is usually what you want, especially since you can tweak that class as much
as you like.
But the reason Evennia knows to fall back to this class is not hard-coded - it's a setting. The default is
in [evennia/settings_default.py](https://github.com/evennia/evennia/blob/master/evennia/settings_default.py#L465),
with the name `BASE_OBJECT_TYPECLASS`, which is set to `typeclasses.objects.Object`.
```{sidebar} Changing things
While it's tempting to change folders around to your liking, this can make it harder to follow tutorials and may confuse if you are asking others for help. So don't overdo it unless you really know what you are doing.
```
So if you wanted the creation commands and methods to default to some other class you could
add your own `BASE_OBJECT_TYPECLASS` line to `mygame/server/conf/settings.py`. The same is true for all the other
typeclasseses, like characters, rooms and accounts. This way you can change the
layout of your game dir considerably if you wanted. You just need to tell Evennia where everything is.
## Modifying ourselves
Let's try to modify ourselves a little. Open up `mygame/typeclasses/characters.py`.
```python
"""
(module docstring)
"""
from evennia import DefaultCharacter
class Character(DefaultCharacter):
"""
(class docstring)
"""
pass
```
This looks quite familiar now - an empty class inheriting from the Evennia base typeclass. As you would expect,
this is also the default typeclass used for creating Characters if you don't specify it. You can verify it:
> examine me
------------------------------------------------------------------------------
Name/key: YourName (#1)
Session id(s): #1
Account: YourName
Account Perms: <Superuser> (quelled)
Typeclass: Character (typeclasses.characters.Character)
Location: Limbo (#2)
Home: Limbo (#2)
Permissions: developer, player
Locks: boot:false(); call:false(); control:perm(Developer); delete:false();
drop:holds(); edit:false(); examine:perm(Developer); get:false();
msg:all(); puppet:false(); tell:perm(Admin); view:all()
Stored Cmdset(s):
commands.default_cmdsets.CharacterCmdSet [DefaultCharacter] (Union, prio 0)
Merged Cmdset(s):
...
Commands available to YourName (result of Merged CmdSets):
...
Persistent attributes:
desc = This is User #1.
prelogout_location = Limbo
Non-Persistent attributes:
last_cmd = None
------------------------------------------------------------------------------
You got a lot longer output this time. You have a lot more going on than a simple Object. Here are some new fields of note:
- **Session id(s)**: This identifies the _Session_ (that is, the individual connection to a player's game client).
- **Account** shows, well the `Account` object associated with this Character and Session.
- **Stored/Merged Cmdsets** and **Commands available** is related to which _Commands_ are stored on you. We will
get to them in the [next lesson](./Beginner-Tutorial-Adding-Commands.md). For now it's enough to know these consitute all the
commands available to you at a given moment.
- **Non-Persistent attributes** are Attributes that are only stored temporarily and will go away on next reload.
Look at the **Typeclass** field and you'll find that it points to `typeclasses.character.Character` as expected.
So if we modify this class we'll also modify ourselves.
### A method on ourselves
Let's try something simple first. Back in `mygame/typeclasses/characters.py`:
```python
class Character(DefaultCharacter):
"""
(class docstring)
"""
str = 10
dex = 12
int = 15
def get_stats(self):
"""
Get the main stats of this character
"""
return self.str, self.dex, self.int
```
> reload
> py self.get_stats()
(10, 12, 15)
```{sidebar} Tuples and lists
- A `list` is written `[a, b, c, d, ...]`. It can be modified after creation.
- A `tuple` is written `(a, b, c, ...)`. It cannot be modified once created.
```
We made a new method, gave it a docstring and had it `return` the RP-esque values we set. It comes back as a
_tuple_ `(10, 12, 15)`. To get a specific value you could specify the _index_ of the value you want,
starting from zero:
> py stats = self.get_stats() ; print(f"Strength is {stats[0]}.")
Strength is 10.
### Attributes
So what happens when we increase our strength? This would be one way:
> py self.str = self.str + 1
> py self.str
11
Here we set the strength equal to its previous value + 1. A shorter way to write this is to use Python's `+=`
operator:
> py self.str += 1
> py self.str
12
> py self.get_stats()
(12, 12, 15)
This looks correct! Try to change the values for dex and int too; it works fine. However:
> reload
> py self.get_stats()
(10, 12, 15)
After a reload all our changes were forgotten. When we change properties like this, it only changes in memory,
not in the database (nor do we modify the python module's code). So when we reloaded, the 'fresh' `Character`
class was loaded, and it still has the original stats we wrote to it.
In principle we could change the python code. But we don't want to do that manually every time. And more importantly
since we have the stats hardcoded in the class, _every_ character instance in the game will have exactly the
same `str`, `dex` and `int` now! This is clearly not what we want.
Evennia offers a special, persistent type of property for this, called an `Attribute`. Rework your
`mygame/typeclasses/characters.py` like this:
```python
class Character(DefaultCharacter):
"""
(class docstring)
"""
def get_stats(self):
"""
Get the main stats of this character
"""
return self.db.str, self.db.dex, self.db.int
```
```{sidebar} Spaces in Attribute name?
What if you want spaces in your Attribute name? Or you want to assign the name of the Attribute on-the fly? Then you can use `.attributes.add(name, value)` instead, for example `self.attributes.add("str", 10)`.
```
We removed the hard-coded stats and added added `.db` for every stat. The `.db` handler makes the stat
into an an Evennia `Attribute`.
> reload
> py self.get_stats()
(None, None, None)
Since we removed the hard-coded values, Evennia don't know what they should be (yet). So all we get back
is `None`, which is a Python reserved word to represent nothing, a no-value. This is different from a normal python
property:
> py self.str
AttributeError: 'Character' object has no attribute 'str'
> py self.db.str
(nothing will be displayed, because it's None)
Trying to get an unknown normal Python property will give an error. Getting an unknown Evennia `Attribute` will
never give an error, but only result in `None` being returned. This is often very practical.
> py self.db.str, self.db.dex, self.db.int = 10, 12, 15
> py self.get_stats()
(10, 12, 15)
> reload
> py self.get_stats()
(10, 12, 15)
Now we set the Attributes to the right values. We can see that things work the same as before, also after a
server reload. Let's modify the strength:
> py self.db.str += 2
> py self.get_stats()
(12, 12, 15)
> reload
> py self.get_stats()
(12, 12, 15)
Our change now survives a reload since Evennia automatically saves the Attribute to the database for us.
### Setting things on new Characters
Things a looking better, but one thing remains strange - the stats start out with a value `None` and we
have to manually set them to something reasonable. In a later lesson we will investigate character-creation
in more detail. For now, let's give every new character some random stats to start with.
We want those stats to be set only once, when the object is first created. For the Character, this method
is called `at_object_creation`.
```{sidebar} __init__ vs at_object_creation
For the `Monster` class we used `__init__` to set up the class. We can't use this for a typeclass because it will be called more than once, at the very least after every reload and maybe more depending on caching. Even if you are familiar with Python, avoid touching `__init__` for typeclasses, the results will not be what you expect.
```
```python
# up by the other imports
import random
class Character(DefaultCharacter):
"""
(class docstring)
"""
def at_object_creation(self):
self.db.str = random.randint(3, 18)
self.db.dex = random.randint(3, 18)
self.db.int = random.randint(3, 18)
def get_stats(self):
"""
Get the main stats of this character
"""
return self.db.str, self.db.dex, self.db.int
```
We imported a new module, `random`. This is part of Python's standard library. We used `random.randint` to
set a random value from 3 to 18 to each stat. Simple, but for some classical RPGs this is all you need!
> reload
> py self.get_stats()
(12, 12, 15)
Hm, this is the same values we set before. They are not random. The reason for this is of course that, as said,
`at_object_creation` only runs _once_, the very first time a character is created. Our character object was already
created long before, so it will not be called again.
It's simple enough to run it manually though:
> self.at_object_creation()
> py self.get_stats()
(5, 4, 8)
Lady luck didn't smile on us for this example; maybe you'll fare better. Evennia has a helper command
`update` that re-runs the creation hook and also cleans up any other Attributes not re-created by `at_object_creation`:
> update self
> py self.get_stats()
(8, 16, 14)
### Updating all Characters in a loop
Needless to say, for your game you are wise to have a feel for what you want to go into the `at_object_creation` hook
before you create a lot of objects (characters in this case). But should it come to that you don't want to have to
go around and re-run the method on everyone manually. For the Python beginner, doing this will also give a chance to
try out Python _loops_. We try them out in multi-line Python mode:
> py
> for a in [1, 2, "foo"]: > print(a)
1
2
foo
A python _for-loop_ allows us to loop over something. Above, we made a _list_ of two numbers and a string. In
every iteration of the loop, the variable `a` becomes one element in turn, and we print that.
For our list, we want to loop over all Characters, and want to call `.at_object_creation` on each. This is how
this is done (still in python multi-line mode):
> from typeclasses.characters import Character
> for char in Character.objects.all()
> char.at_object_creation()
```{sidebar} Database queries
`Character.objects.all()` is an example of a database query expressed in Python. This will be converted into a database query under the hood. This syntax is part of [Django's query language](https://docs.djangoproject.com/en/4.1/topics/db/queries/). You don't need to know Django to use Evennia, but if you ever need more specific database queries, this is always available when you need it.
```
We import the `Character` class and then we use `.objects.all()` to get all `Character` instances. Simplified,
`.objects` is a resource from which one can _query_ for all `Characters`. Using `.all()` gets us a listing
of all of them that we then immediately loop over. Boom, we just updated all Characters, including ourselves:
> quit()
Closing the Python console.
> self.get_stats()
(3, 18, 10)
## Extra Credits
This principle is the same for other typeclasses. So using the tools explored in this lesson, try to expand
the default room with an `is_dark` flag. It can be either `True` or `False`.
Have all new rooms start with `is_dark = False` and make it so that once you change it, it survives a reload.
Oh, and if you created any other rooms before, make sure they get the new flag too!
## Conclusions
In this lesson we created database-persistent dragons by having their classes inherit from one `Object`, one
of Evennia's _typeclasses_. We explored where Evennia looks for typeclasses if we don't specify the path
explicitly. We then modified ourselves - via the `Character` class - to give us some simple RPG stats. This
led to the need to use Evennia's _Attributes_, settable via `.db` and to use a for-loop to update ourselves.
Typeclasses are a fundamental part of Evennia and we will see a lot of more uses of them in the course of
this tutorial. But that's enough of them for now. It's time to take some action. Let's learn about _Commands_.

View file

@ -1,483 +0,0 @@
# Parsing Command input
In this lesson we learn some basics about parsing the input of Commands. We will
also learn how to add, modify and extend Evennia's default commands.
## More advanced parsing
In the last lesson we made a `hit` Command and hit a dragon with it. You should have the code
from that still around.
Let's expand our simple `hit` command to accept a little more complex input:
hit <target> [[with] <weapon>]
That is, we want to support all of these forms
hit target
hit target weapon
hit target with weapon
If you don't specify a weapon you'll use your fists. It's also nice to be able to skip "with" if
you are in a hurry. Time to modify `mygame/commands/mycommands.py` again. Let us break out the parsing
a little, in a new method `parse`:
```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 a Command to an object
The commands of a cmdset attached to an object with `obj.cmdset.add()` will by default be made available to that object
but _also to those in the same location as that object_. If you did the [Building introduction](./Beginner-Tutorial-Building-Quickstart.md)
you've seen an example of this with the "Red Button" object. The [Tutorial world](./Beginner-Tutorial-Tutorial-World.md)
also has many examples of objects with commands on them.
To show how this could work, let's put our 'hit' Command on our simple `sword` object from the previous section.
> self.search("sword").cmdset.add("commands.mycommands.MyCmdSet", persistent=True)
We find the sword (it's still in our inventory so `self.search` should be able to find it), then
add `MyCmdSet` to it. This actually adds both `hit` and `echo` to the sword, which is fine.
Let's try to swing it!
> hit
More than one match for 'hit' (please narrow target):
hit-1 (sword #11)
hit-2
```{sidebar} Multi-matches
Some game engines will just pick the first hit when finding more than one. Evennia will always give you a choice. The reason for this is that Evennia cannot know if `hit` and `hit` are different or the same - maybe it behaves differently depending on the object it sits on? Besides, imagine if you had a red and a blue button both with the command `push` on it. Now you just write `push`. Wouldn't you prefer to be asked `which` button you really wanted to push?
```
Woah, that didn't go as planned. Evennia actually found _two_ `hit` commands to didn't know which one to use
(_we_ know they are the same, but Evennia can't be sure of that). As we can see, `hit-1` is the one found on
the sword. The other one is from adding `MyCmdSet` to ourself earlier. It's easy enough to tell Evennia which
one you meant:
> hit-1
Who do you want to hit?
> hit-2
Who do you want to hit?
In this case we don't need both command-sets, so let's just keep the one on the sword:
> self.cmdset.remove("commands.mycommands.MyCmdSet")
> hit
Who do you want to hit?
Now try this:
> tunnel n = kitchen
> n
> drop sword
> s
> hit
Command 'hit' is not available. Maybe you meant ...
> n
> hit
Who do you want to hit?
The `hit` command is now only available if you hold or are in the same room as the sword.
### You need to hold the sword!
Let's get a little ahead of ourselves and make it so you have to _hold_ the sword for the `hit` command to
be available. This involves a _Lock_. We've cover locks in more detail later, just know that they are useful
for limiting the kind of things you can do with an object, including limiting just when you can call commands on
it.
```{sidebar} Locks
Evennia Locks are defined as a mini-language defined in `lockstrings`. The lockstring is on a form `<situation>:<lockfuncs>`, where `situation` determines when this lock applies and the `lockfuncs` (there can be more than one) are run to determine if the lock-check passes or not depending on circumstance.
```
> py self.search("sword").locks.add("call:holds()")
We added a new lock to the sword. The _lockstring_ `"call:holds()"` means that you can only _call_ commands on
this object if you are _holding_ the object (that is, it's in your inventory).
For locks to work, you cannot be _superuser_, since the superuser passes all locks. You need to `quell` yourself
first:
```{sidebar} quell/unquell
Quelling allows you as a developer to take on the role of players with less priveleges. This is useful for testing and debugging, in particular since a superuser has a little `too` much power sometimes. Use `unquell` to get back to your normal self.
```
> quell
If the sword lies on the ground, try
> hit
Command 'hit' is not available. ..
> get sword
> hit
> Who do you want to hit?
Finally, we get rid of ours sword so we have a clean slate with no more `hit` commands floating around.
We can do that in two ways:
delete sword
or
py self.search("sword").delete()
## Adding the Command to a default Cmdset
As we have seen we can use `obj.cmdset.add()` to add a new cmdset to objects, whether that object
is ourself (`self`) or other objects like the `sword`.
This is how all commands in Evennia work, including default commands like `look`, `dig`, `inventory` and so on.
All these commands are in just loaded on the default objects that Evennia provides out of the box.
- Characters (that is 'you' in the gameworld) has the `CharacterCmdSet`.
- Accounts (the thing that represents your out-of-character existence on the server) has the `AccountCmdSet`
- Sessions (representing one single client connection) has the `SessionCmdSet`
- Before you log in (at the connection screen) you'll have access to the `UnloggedinCmdSet`.
The thing must commonly modified is the `CharacterCmdSet`.
The default cmdset are defined in `mygame/commands/default_cmdsets.py`. Open that file now:
```python
"""
(module docstring)
"""
from evennia import default_cmds
class CharacterCmdSet(default_cmds.CharacterCmdSet):
key = "DefaultCharacter"
def at_cmdset_creation(self):
super().at_cmdset_creation()
#
# any commands you add below will overload the default ones
#
class AccountCmdSet(default_cmds.AccountCmdSet):
key = "DefaultAccount"
def at_cmdset_creation(self):
super().at_cmdset_creation()
#
# any commands you add below will overload the default ones
#
class UnloggedinCmdSet(default_cmds.UnloggedinCmdSet):
key = "DefaultUnloggedin"
def at_cmdset_creation(self):
super().at_cmdset_creation()
#
# any commands you add below will overload the default ones
#
class SessionCmdSet(default_cmds.SessionCmdSet):
key = "DefaultSession"
def at_cmdset_creation(self):
super().at_cmdset_creation()
#
# any commands you add below will overload the default ones
#
```
```{sidebar} super()
The `super()` function refers to the parent of the current class and is commonly used to call same-named methods on the parent.
```
`evennia.default_cmds` is a container that holds all of Evennia's default commands and cmdsets. In this module
we can see that this was imported and then a new child class was made for each cmdset. Each class looks familiar
(except the `key`, that's mainly used to easily identify the cmdset in listings). In each `at_cmdset_creation` all
we do is call `super().at_cmdset_creation` which means that we call `at_cmdset_creation() on the _parent_ CmdSet.
This is what adds all the default commands to each CmdSet.
To add even more Commands to a default cmdset, we can just add them below the `super()` line. Usefully, if we were to
add a Command with the same `.key` as a default command, it would completely replace that original. So if you were
to add a command with a key `look`, the original `look` command would be replaced by your own version.
For now, let's add our own `hit` and `echo` commands to the `CharacterCmdSet`:
```python
# ...
from commands import mycommands
class CharacterCmdSet(default_cmds.CharacterCmdSet):
key = "DefaultCharacter"
def at_cmdset_creation(self):
super().at_cmdset_creation()
#
# any commands you add below will overload the default ones
#
self.add(mycommands.CmdEcho)
self.add(mycommands.CmdHit)
```
> reload
> hit
Who do you want to hit?
Your new commands are now available for all player characters in the game. There is another way to add a bunch
of commands at once, and that is to add a _CmdSet_ to the other cmdset. All commands in that cmdset will then be added:
```python
from commands import mycommands
class CharacterCmdSet(default_cmds.CharacterCmdSet):
key = "DefaultCharacter"
def at_cmdset_creation(self):
super().at_cmdset_creation()
#
# any commands you add below will overload the default ones
#
self.add(mycommands.MyCmdSet)
```
Which way you use depends on how much control you want, but if you already have a CmdSet,
this is practical. A Command can be a part of any number of different CmdSets.
### Removing Commands
To remove your custom commands again, you of course just delete the change you did to
`mygame/commands/default_cmdsets.py`. But what if you want to remove a default command?
We already know that we use `cmdset.remove()` to remove a cmdset. It turns out you can
do the same in `at_cmdset_creation`. For example, let's remove the default `get` Command
from Evennia. We happen to know this can be found as `default_cmds.CmdGet`.
```python
# ...
from commands import mycommands
class CharacterCmdSet(default_cmds.CharacterCmdSet):
key = "DefaultCharacter"
def at_cmdset_creation(self):
super().at_cmdset_creation()
#
# any commands you add below will overload the default ones
#
self.add(mycommands.MyCmdSet)
self.remove(default_cmds.CmdGet)
# ...
```
> reload
> get
Command 'get' is not available ...
## Replace a default command
At this point you already have all the pieces for how to do this! We just need to add a new
command with the same `key` in the `CharacterCmdSet` to replace the default one.
Let's combine this with what we know about classes and
how to _override_ a parent class. Open `mygame/commands/mycommands.py` and lets override
that `CmdGet` command.
```python
# up top, by the other imports
from evennia import default_cmds
# somewhere below
class MyCmdGet(default_cmds.CmdGet):
def func(self):
super().func()
self.caller.msg(str(self.caller.location.contents))
```
- **Line2**: We import `default_cmds` so we can get the parent class.
We made a new class and we make it _inherit_ `default_cmds.CmdGet`. We don't
need to set `.key` or `.parse`, that's already handled by the parent.
In `func` we call `super().func()` to let the parent do its normal thing,
- **Line 7**: By adding our own `func` we replace the one in the parent.
- **Line 8**: For this simple change we still want the command to work the
same as before, so we use `super()` to call `func` on the parent.
- **Line 9**: `.location` is the place an object is at. `.contents` contains, well, the
contents of an object. If you tried `py self.contents` you'd get a list that equals
your inventory. For a room, the contents is everything in it.
So `self.caller.location.contents` gets the contents of our current location. This is
a _list_. In order send this to us with `.msg` we turn the list into a string. Python
has a special function `str()` to do this.
We now just have to add this so it replaces the default `get` command. Open
`mygame/commands/default_cmdsets.py` again:
```python
# ...
from commands import mycommands
class CharacterCmdSet(default_cmds.CharacterCmdSet):
key = "DefaultCharacter"
def at_cmdset_creation(self):
super().at_cmdset_creation()
#
# any commands you add below will overload the default ones
#
self.add(mycommands.MyCmdSet)
self.add(mycommands.MyCmdGet)
# ...
```
```{sidebar} Another way
Instead of adding `MyCmdGet` explicitly in default_cmdset.py, you could also add it to `mycommands.MyCmdSet` and let it be added automatically here for you.
```
> reload
> get
Get What?
[smaug, fluffy, YourName, ...]
We just made a new `get`-command that tells us everything we could pick up (well, we can't pick up ourselves, so
there's some room for improvement there).
## Summary
In this lesson we got into some more advanced string formatting - many of those tricks will help you a lot in
the future! We also made a functional sword. Finally we got into how to add to, extend and replace a default
command on ourselves.

View file

@ -1,63 +0,0 @@
# Part 1: What we have
```{sidebar} Beginner Tutorial Parts
- [Introduction](../Beginner-Tutorial-Intro.md)
<br>Getting set up.
- Part 1: **[What we have](./Beginner-Tutorial-Part1-Intro.md)**
<br>A tour of Evennia and how to use the tools, including an introduction to Python.
- Part 2: [What we want](../Part2/Beginner-Tutorial-Part2-Intro.md)
<br>Planning our tutorial game and what to think about when planning your own in the future.
- Part 3: [How we get there](../Part3/Beginner-Tutorial-Part3-Intro.md)
<br>Getting down to the meat of extending Evennia to make our game
- Part 4: [Using what we created](../Part4/Beginner-Tutorial-Part4-Intro.md)
<br>Building a tech-demo and world content to go with our code
- Part 5: [Showing the world](../Part5/Beginner-Tutorial-Part5-Intro.md)
<br>Taking our new game online and let players try it out
```
In this first part we'll focus on what we get out of the box in Evennia - we'll get used to the tools,
and how to find things we are looking for. We will also dive into some of things you'll
need to know to fully utilize the system, including giving you a brief rundown of Python concepts. If you are
an experienced Python programmer, some sections may feel a bit basic, but you will at least not have seen
these concepts in the context of Evennia before.
## Lessons
```{toctree}
:maxdepth: 1
:numbered:
Beginner-Tutorial-Building-Quickstart
Beginner-Tutorial-Tutorial-World
Beginner-Tutorial-Python-basic-introduction
Beginner-Tutorial-Gamedir-Overview
Beginner-Tutorial-Python-classes-and-objects
Beginner-Tutorial-Evennia-Library-Overview
Beginner-Tutorial-Learning-Typeclasses
Beginner-Tutorial-Adding-Commands
Beginner-Tutorial-More-on-Commands
Beginner-Tutorial-Creating-Things
Beginner-Tutorial-Searching-Things
Beginner-Tutorial-Django-queries
```
## Table of Contents
```{toctree}
:maxdepth: 2
Beginner-Tutorial-Building-Quickstart
Beginner-Tutorial-Tutorial-World
Beginner-Tutorial-Python-basic-introduction
Beginner-Tutorial-Gamedir-Overview
Beginner-Tutorial-Python-classes-and-objects
Beginner-Tutorial-Evennia-Library-Overview
Beginner-Tutorial-Learning-Typeclasses
Beginner-Tutorial-Adding-Commands
Beginner-Tutorial-More-on-Commands
Beginner-Tutorial-Creating-Things
Beginner-Tutorial-Searching-Things
Beginner-Tutorial-Django-queries
```

View file

@ -1,649 +0,0 @@
# Intro to using Python with Evennia
Time to dip our toe into some coding! Evennia is written and extended in [Python](https://python.org),
which is a mature and professional programming language that is very fast to work with.
That said, even though Python is widely considered easy to learn, we can only cover the most immediately
important aspects of Python in this series of starting tutorials. Hopefully we can get you started
but then you'll need to continue learning from there. See our [link section](../../../Links.md) for finding
more reference material and dedicated Python tutorials.
> While this will be quite basic if you are an experienced developer, you may want to at least
> stay around for the first few sections where we cover how to run Python from inside Evennia.
First, if you were quelling yourself to play the tutorial world, make sure to get your
superuser powers back:
unquell
## Evennia Hello world
The `py` Command (or `!`, which is an alias) allows you as a superuser to execute raw Python from in-
game. This is useful for quick testing. From the game's input line, enter the following:
> py print("Hello World!")
```{sidebar} Command input
The line with `>` indicates input to enter in-game, while the lines below are the
expected return from that input.
```
You will see
> print("Hello world!")
Hello World!
To understand what is going on: some extra info: The `print(...)` *function* is the basic, in-built
way to output text in Python. We are sending "Hello World" as an _argument_ to this function. The quotes `"..."`
mean that you are inputting a *string* (i.e. text). You could also have used single-quotes `'...'`,
Python accepts both. A third variant is triple-quotes (`"""..."""` or `'''...'''`, which work across multiple
lines and are common for larger text-blocks. The way we use the `py` command right now only supports
single-line input however.
## Making some text 'graphics'
When making a text-game you will, unsurprisingly, be working a lot with text. Even if you have the occational
button or even graphical element, the normal process is for the user to input commands as
text and get text back. As we saw above, a piece of text is called a _string_ in Python and is enclosed in
either single- or double-quotes.
Strings can be added together:
> py print("This is a " + "breaking change.")
This is a breaking change.
A string multiplied with a number will repeat that string as many times:
> py print("|" + "-" * 40 + "|")
|----------------------------------------|
or
> py print("A" + "a" * 5 + "rgh!")
Aaaaaargh!
### .format()
While combining different strings is useful, even more powerful is the ability to modify the contents
of the string in-place. There are several ways to do this in Python and we'll show two of them here. The first
is to use the `.format` _method_ of the string:
> py print("This is a {} idea!".format("good"))
This is a good idea!
```{sidebar} Functions and Methods
- Function: Something that performs and action when you `call` it with zero or more `arguments`. A function is stand-alone in a python module, like `print()`
- Method: A function that sits "on" an object, like `obj.msg()`.
```
A method can be thought of as a resource "on" another object. The method knows on which object it
sits and can thus affect it in various ways. You access it with the period `.`. In this case, the
string has a resource `format(...)` that modifies it. More specifically, it replaced the `{}` marker
inside the string with the value passed to the format. You can do so many times:
> py print("This is a {} idea!".format("bad"))
This is a bad idea!
or
> py print("This is the {} and {} {} idea!".format("first", "second", "great"))
This is the first and second great idea!
> Note the double-parenthesis at the end - the first closes the `format(...` method and the outermost
closes the `print(...`. Not closing them will give you a scary `SyntaxError`. We will talk a
little more about errors in the next section, for now just fix until it prints as expected.
Here we passed three comma-separated strings as _arguments_ to the string's `format` method. These
replaced the `{}` markers in the same order as they were given.
The input does not have to be strings either:
> py print("STR: {}, DEX: {}, INT: {}".format(12, 14, 8))
STR: 12, DEX: 14, INT: 8
To separate two Python instructions on the same line, you use the semi-colon, `;`. Try this:
> py a = "awesome sauce" ; print("This is {}!".format(a))
This is awesome sauce!
```{warning} MUD clients and semi-colon
Some MUD clients use the semi-colon `;` to split client-inputs
into separate sends. If so, the above will give an error. Most clients allow you to
run in 'verbatim' mode or to remap to use some other separator than `;`. If you still have
trouble, use the Evennia web client.
```
What happened here was that we _assigned_ the string `"awesome sauce"` to a _variable_ we chose
to name `a`. In the next statement, Python remembered what `a` was and we passed that into `format()`
to get the output. If you replaced the value of `a` with something else in between, _that_ would be printed
instead.
Here's the stat-example again, moving the stats to variables (here we just set them, but in a real
game they may be changed over time, or modified by circumstance):
> py stren, dext, intel = 13, 14, 8 ; print("STR: {}, DEX: {}, INT: {}".format(stren, dext, intel))
STR: 13, DEX: 14, INT: 8
The point is that even if the values of the stats change, the print() statement would not change - it just keeps
pretty-printing whatever is given to it.
### f-strings
Using `.format()` is convenient (and there is a [lot more](https://www.w3schools.com/python/ref_string_format.asp)
you can do with it). But the _f-string_ can be even more convenient. An
f-string looks like a normal string ... except there is an `f` front of it, like this:
f"this is now an f-string."
An f-string on its own is just like any other string. But let's redo the example we did before, using an f-string:
> py a = "awesome sauce" ; print(f"This is {a}!")
This is awesome sauce!
We could just insert that `a` variable directly into the f-string using `{a}`. Fewer parentheses to
remember and arguable easier to read as well.
> py stren, dext, intel = 13, 14, 8 ; print(f"STR: {stren}, DEX: {dext}, INT: {intel}")
STR: 13, DEX: 14, INT: 8
We will be exploring more complex string concepts when we get to creating Commands and need to
parse and understand player input.
### Colored text
Python itself knows nothing about colored text, this is an Evennia thing. Evennia supports the
standard color schemes of traditional MUDs.
> py print("|rThis is red text!|n This is normal color.")
Adding that `|r` at the start will turn our output bright red. `|R` will make it dark red. `|n`
gives the normal text color. You can also use RGB (Red-Green-Blue) values from 0-5 (Xterm256 colors):
> py print("|043This is a blue-green color.|[530|003 Now dark blue text on orange background.")
> If you don't see the expected color, your client or terminal may not support Xterm256 (or
color at all). Use the Evennia webclient.
Use the commands `color ansi` or `color xterm` to see which colors are available. Experiment!
## Importing code from other modules
As we saw in the previous sections, we used `.format` to format strings and `me.msg` to access
the `msg` method on `me`. This use of the full-stop character is used to access all sorts of resources,
including that in other Python modules.
Keep your game running, then open a text editor of your choice. If your game folder is called
`mygame`, create a new text file `test.py` in the subfolder `mygame/world`. This is how the file
structure should look:
```
mygame/
world/
test.py
```
For now, only add one line to `test.py`:
```python
print("Hello World!")
```
```{sidebar} Python module
This is a text file with the `.py` file ending. A module
contains Python source code and from within Python one can
access its contents by importing it via its python-path.
```
Don't forget to _save_ the file. We just created our first Python _module_!
To use this in-game we have to *import* it. Try this:
> py import world.test
Hello World
If you make some error (we'll cover how to handle errors below), fix the error in the module and
run the `reload` command in-game for your changes to take effect.
So importing `world.test` actually means importing `world/test.py`. Think of the period `.` as
replacing `/` (or `\` for Windows) in your path. The `.py` ending of `test.py` is also never
included in this "Python-path", but _only_ files with that ending can be imported this way.
Where is `mygame` in that Python-path? The answer is that Evennia has already told Python that
your `mygame` folder is a good place to look for imports. So we don't include `mygame` in the
path - Evennia handles this for us.
When you import the module, the top "level" of it will execute. In this case, it will immediately
print "Hello World".
Now try to run this a second time:
> py import world.test
You will *not* see any output this second time or any subsequent times! This is not a bug. Rather
it is because of how Python importing works - it stores all imported modules and will
avoid importing them more than once. So your `print` will only run the first time, when the module
is first imported.
Try this:
> reload
And then
> py import world.test
Hello World!
Now we see it again. The `reload` wiped the server's memory of what was imported, so it had to
import it anew. You'd have to do this every time you wanted the print to show though, which is
not very useful.
> We'll get back to more advanced ways to import code in later tutorial sections - this is an
> important topic. But for now, let's press on and resolve this particular problem.
### Our first own function
We want to be able to print our hello-world message at any time, not just once after a server
reload. Change your `mygame/world/test.py` file to look like this:
```python
def hello_world():
print("Hello World!")
```
As we are moving to multi-line Python code, there are some important things to remember:
- Capitalization matters in Python. It must be `def` and not `DEF`, `who` is not the same as `Who`.
- Indentation matters in Python. The second line must be indented or it's not valid code. You should
also use a consistent indentation length. We *strongly* recommend that you, for your own sanity's sake,
set up your editor to always indent *4 spaces* (**not** a single tab-character) when you press the TAB key.
So about that function. Line 1:
- `def` is short for "define" and defines a *function* (or a *method*, if sitting on an object).
This is a [reserved Python keyword](https://docs.python.org/2.5/ref/keywords.html); try not to use
these words anywhere else.
- A function name can not have spaces but otherwise we could have called it almost anything. We call
it `hello_world`. Evennia follows [Python's standard naming style](https://github.com/evennia/evennia/blob/master/CODING_STYLE.md#a-quick-list-of-code-style-points)
with lowercase letters and underscores. We recommend you do the same.
- The colon (`:`) at the end of line 1 indicates that the header of the function is complete.
Line 2:
- The indentation marks the beginning of the actual operating code of the function (the function's
*body*). If we wanted more lines to belong to this function those lines would all have to
start at least at this indentation level.
Now let's try this out. First `reload` your game to have it pick up
our updated Python module, then import it.
> reload
> py import world.test
Nothing happened! That is because the function in our module won't do anything just by importing it (this
is what we wanted). It will only act when we *call* it. So we need to first import the module and then access the
function within:
> py import world.test ; world.test.hello_world()
Hello world!
There is our "Hello World"! As mentioned earlier, use use semi-colon to put multiple
Python-statements on one line. Note also the previous warning about mud-clients using the `;` to their
own ends.
So what happened there? First we imported `world.test` as usual. But this time we continued and
accessed the `hello_world` function _inside_ the newly imported module.
By adding `()` to the `hello_world` function we _call_ it, that is we run the body of the function and
print our text. We can now redo this as many times as we want without having to `reload` in between:
> py import world.test ; world.test.hello_world()
Hello world!
> py import world.test ; world.test.hello_world()
Hello world!
## Sending text to others
The `print` command is a standard Python structure. We can use that here in the `py` command since
we can se the output. It's great for debugging and quick testing. But if you need to send a text
to an actual player, `print` won't do, because it doesn't know _who_ to send to. Try this:
> py me.msg("Hello world!")
Hello world!
This looks the same as the `print` result, but we are now actually messaging a specific *object*,
`me`. The `me` is a shortcut to 'us', the one running the `py` command. It is not some special
Python thing, but something Evennia just makes available in the `py` command for convenience
(`self` is an alias).
The `me` is an example of an *Object instance*. Objects are fundamental in Python and Evennia.
The `me` object also contains a lot of useful resources for doing
things with that object. We access those resources with '`.`'.
One such resource is `msg`, which works like `print` except it sends the text to the object it
is attached to. So if we, for example, had an object `you`, doing `you.msg(...)` would send a message
to the object `you`.
For now, `print` and `me.msg` behaves the same, just remember that `print` is mainly used for
debugging and `.msg()` will be more useful for you in the future.
## Parsing Python errors
Let's try this new text-sending in the function we just created. Go back to
your `test.py` file and Replace the function with this instead:
```python
def hello_world():
me.msg("Hello World!")
```
Save your file and `reload` your server to tell Evennia to re-import new code,
then run it like before:
> py import world.test ; world.test.hello_world()
No go - this time you get an error!
```python
File "./world/test.py", line 2, in hello_world
me.msg("Hello World!")
NameError: name 'me' is not defined
```
```{sidebar} Errors in the logs
In regular use, tracebacks will often appear in the log rather than
in the game. Use `evennia --log` to view the log in the terminal. Make
sure to scroll back if you expect an error and don't see it. Use
`Ctrl-C` (or `Cmd-C` on Mac) to exit the log-view.
```
This is called a *traceback*. Python's errors are very friendly and will most of the time tell you
exactly what and where things go wrong. It's important that you learn to parse tracebacks so you
know how to fix your code.
A traceback is to be read from the _bottom up_:
- (line 3) An error of type `NameError` is the problem ...
- (line 3) ... more specifically it is due to the variable `me` not being defined.
- (line 2) This happened on the line `me.msg("Hello world!")` ...
- (line 1) ... which is on line `2` of the file `./world/test.py`.
In our case the traceback is short. There may be many more lines above it, tracking just how
different modules called each other until the program got to the faulty line. That can
sometimes be useful information, but reading from the bottom is always a good start.
The `NameError` we see here is due to a module being its own isolated thing. It knows nothing about
the environment into which it is imported. It knew what `print` is because that is a special
[reserved Python keyword](https://docs.python.org/2.5/ref/keywords.html). But `me` is *not* such a
reserved word (as mentioned, it's just something Evennia came up with for convenience in the `py`
command). As far as the module is concerned `me` is an unfamiliar name, appearing out of nowhere.
Hence the `NameError`.
## Passing arguments to functions
We know that `me` exists at the point when we run the `py` command, because we can do `py me.msg("Hello World!")`
with no problem. So let's _pass_ that me along to the function so it knows what it should be.
Go back to your `test.py` and change it to this:
```python
def hello_world(who):
who.msg("Hello World!")
```
We now added an _argument_ to the function. We could have named it anything. Whatever `who` is,
we will call a method `.msg()` on it.
As usual, `reload` the server to make sure the new code is available.
> py import world.test ; world.test.hello_world(me)
Hello World!
Now it worked. We _passed_ `me` to our function. It will appear inside the function renamed as `who` and
now the function works and prints as expected. Note how the `hello_world` function doesn't care _what_ you
pass into it as long as it has a `.msg()` method on it. So you could reuse this function over and over for other
suitable targets.
> **Extra Credit:** As an exercise, try to pass something else into `hello_world`. Try for example
>to pass the number `5` or the string `"foo"`. You'll get errors telling you that they don't have
>the attribute `msg`. They don't care about `me` itself not being a string or a number. If you are
>familiar with other programming languages (especially C/Java) you may be tempted to start *validating*
>`who` to make sure it's of the right type before you send it. This is usually not recommended in Python.
>Python philosophy is to [handle](https://docs.python.org/2/tutorial/errors.html) the error if it happens
>rather than to add a lot of code to prevent it from happening. See [duck typing](https://en.wikipedia.org/wiki/Duck_typing)
>and the concept of _Leap before you Look_.
## Finding others to send to
Let's wrap up this first Python `py` crash-course by finding someone else to send to.
In Evennia's `contrib/` folder (`evennia/contrib/tutorial_examples/mirror.py`) is a handy little
object called the `TutorialMirror`. The mirror will echo whatever is being sent to it to
the room it is in.
On the game command-line, let's create a mirror:
> create/drop mirror:contrib.tutorial_examples.mirror.TutorialMirror
```{sidebar} Creating objects
The `create` command was first used to create boxes in the
`Building Stuff <Building-Quickstart>`_ tutorial. Note how it
uses a "python-path" to describe where to load the mirror's code from.
```
A mirror should appear in your location.
> look mirror
mirror shows your reflection:
This is User #1
What you are seeing is actually your own avatar in the game, the same thing that is available as `me` in the `py`
command.
What we are aiming for now is the equivalent of `mirror.msg("Mirror Mirror on the wall")`. But the first thing that
comes to mind will not work:
> py mirror.msg("Mirror, Mirror on the wall ...")
NameError: name 'mirror' is not defined.
This is not surprising: Python knows nothing about "mirrors" or locations or anything. The `me` we've been using
is, as mentioned, just a convenient thing the Evennia devs makes available to the `py` command. They couldn't possibly
predict that you wanted to talk to mirrors.
Instead we will need to _search_ for that `mirror` object before we can send to it.
Make sure you are in the same location as the mirror and try:
> py me.search("mirror")
mirror
`me.search("name")` will, by default, search and _return_ an object with the given name found in _the same location_
as the `me` object is. If it can't find anything you'll see an error.
```{sidebar} Function returns
Whereas a function like `print` only prints its arguments, it's very common
for functions/methods to `return` a result of some kind. Think of the function
as a machine - you put something in and out comes a result you can use. In the case
of `me.search`, it will perform a database search and spit out the object it finds.
```
> py me.search("dummy")
Could not find 'dummy'.
Wanting to find things in the same location is very common, but as we continue we'll
find that Evennia provides ample tools for tagging, searching and finding things from all over your game.
Now that we know how to find the 'mirror' object, we just need to use that instead of `me`!
> py mirror = self.search("mirror") ; mirror.msg("Mirror, Mirror on the wall ...")
mirror echoes back to you:
"Mirror, Mirror on the wall ..."
The mirror is useful for testing because its `.msg` method just echoes whatever is sent to it back to the room. More common
would be to talk to a player character, in which case the text you sent would have appeared in their game client.
## Multi-line py
So far we have use `py` in single-line mode, using `;` to separate multiple inputs. This is very convenient
when you want to do some quick testing. But you can also start a full multi-line Python interactive interpreter
inside Evennia.
> py
Evennia Interactive Python mode
Python 3.7.1 (default, Oct 22 2018, 11:21:55)
[GCC 8.2.0] on Linux
[py mode - quit() to exit]
(the details of the output will vary with your Python version and OS). You are now in python interpreter mode. It means
that _everything_ you insert from now on will become a line of Python (you can no longer look around or do other
commands).
> print("Hello World")
>>> print("Hello World")
Hello World
[py mode - quit() to exit]
Note that we didn't need to put `py` in front now. The system will also echo your input (that's the bit after
the `>>>`). For brevity in this tutorual we'll turn the echo off. First exit `py` and then start again with the
`/noecho` flag.
> quit()
Closing the Python console.
> py/noecho
Evennia Interactive Python mode (no echoing of prompts)
Python 3.7.1 (default, Oct 22 2018, 11:21:55)
[GCC 8.2.0] on Linux
[py mode - quit() to exit]
```{sidebar} interactive py
- Start with `py`.
- Use `py/noecho` if you don't want your input to be echoed for every line.
- All your inputs will now be interpreted as Python code.
- Exit with `quit()`.
```
We can now enter multi-line Python code:
> a = "Test"
> print(f"This is a {a}."}
This is a Test.
Let's try to define a function:
> def hello_world(who, txt):
...
> who.msg(txt)
...
>
[py mode - quit() to exit]
Some important things above:
- Definining a function with `def` means we are starting a new code block. Python works so that you mark the content
of the block with indention. So the next line must be manually indented (4 spaces is a good standard) in order
for Python to know it's part of the function body.
- We expand the `hello_world` function with another argument `txt`. This allows us to send any text, not just
"Hello World" over and over.
- To tell `py` that no more lines will be added to the function body, we end with an empty input. When
the normal prompt on how to exit returns, we know we are done.
Now we have defined a new function. Let's try it out:
> hello_world(me, "Hello world to me!")
Hello world to me!
The `me` is still available to us, so we pass that as the `who` argument, along with a little longer
string. Let's combine this with searching for the mirror.
> mirror = me.search("mirror")
> hello_world(mirror, "Mirror, Mirror on the wall ...")
mirror echoes back to you:
"Mirror, Mirror on the wall ..."
Exit the `py` mode with
> quit()
Closing the Python console.
## Other ways to test Python code
The `py` command is very powerful for experimenting with Python in-game. It's great for quick testing.
But you are still limited to working over telnet or the webclient, interfaces that doesn't know anything
about Python per-se.
Outside the game, go to the terminal where you ran Evennia (or any terminal where the `evennia` command
is available).
- `cd` to your game dir.
- `evennia shell`
A Python shell opens. This works like `py` did inside the game, with the exception that you don't have
`me` available out of the box. If you want `me`, you need to first find yourself:
> import evennia
> me = evennia.search_object("YourChar")[0]
Here we make use of one of evennia's search functions, available by importing `evennia` directly.
We will cover more advanced searching later, but suffice to say, you put your own character name instead of
"YourChar" above.
> The `[0]` at the end is because `.search_object` returns a list of objects and we want to
get at the first of them (counting starts from 0).
Use `Ctrl-D` (`Cmd-D` on Mac) or `quit()` to exit the Python console.
## ipython
The default Python shell is quite limited and ugly. It's *highly* recommended to install `ipython` instead. This
is a much nicer, third-party Python interpreter with colors and many usability improvements.
pip install ipython
If `ipython` is installed, `evennia shell` will use it automatically.
evennia shell
...
IPython 7.4.0 -- An enhanced Interactive Python. Type '?' for help
In [1]: You now have Tab-completion:
> import evennia
> evennia.<TAB>
That is, enter `evennia.` and then press the TAB key - you will be given a list of all the resources
available on the `evennia` object. This is great for exploring what Evennia has to offer. For example,
use your arrow keys to scroll to `search_object()` to fill it in.
> evennia.search_object?
Adding a `?` and pressing return will give you the full documentation for `.search_object`. Use `??` if you
want to see the entire source code.
As for the normal python interpreter, use `Ctrl-D`/`Cmd-D` or `quit()` to exit ipython.
```{important} Persistent code
Common for both `py` and `python`/`ipython` is that the code you write is not persistent - it will
be gone after you shut down the interpreter (but ipython will remember your input history). For making long-lasting
Python code, we need to save it in a Python module, like we did for `world/test.py`.
```
## Conclusions
This covers quite a lot of basic Python usage. We printed and formatted strings, defined our own
first function, fixed an error and even searched and talked to a mirror! Being able to access
python inside and outside of the game is an important skill for testing and debugging, but in
practice you will be writing most your code in Python modules.
To that end we also created a first new Python module in the `mygame/` game dir, then imported and used it.
Now let's look at the rest of the stuff you've got going on inside that `mygame/` folder ...

View file

@ -1,404 +0,0 @@
# Introduction to Python classes and objects
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.
## Importing things
No one writes something as big as an online game in one single huge file. Instead one breaks up the
code into separate files (modules). Each module is dedicated to different purposes. Not only does
it make things cleaner, organized and easier to understand. It also makes it easier to re-use code -
you just import the resources you need and know you only get just what you requested. This makes
it much easier to find errors and to know what code is good and which has issues.
> Evennia itself uses your code in the same way - you just tell it where a particular type of code is,
and it will import and use it (often instead of its defaults).
We have already successfully imported things, for example:
> py import world.test ; world.test.hello_world(me)
Hello World!
In this example, on your hard drive, the files looks like this:
```
mygame/
world/
test.py <- inside this file is a function hello_world
```
If you followed earlier tutorial lessons, the `mygame/world/test.py` file should look like this (if
not, make it so):
```python
def hello_world(who):
who.msg("Hello World!")
```
```{sidebar} Whitespace matters in Python!
- Indentation matters in Python
- So does capitalization
- Use 4 `spaces` to indent, not tabs
- Empty lines are fine
- Anything on a line after a `#` is a `comment`, ignored by Python
```
The _python_path_ describes the relation between Python resources, both between and inside
Python _modules_ (that is, files ending with .py). A python-path separates each part of the
path `.` and always skips the `.py` file endings. Also, Evennia already knows to start looking
for python resources inside `mygame/` so this should never be specified. Hence
import world.test
The `import` Python instruction loads `world.test` so you have it available. You can now go "into"
this module to get to the function you want:
world.test.hello_world(me)
Using `import` like this means that you have to specify the full `world.test` every time you want
to get to your function. Here's a more powerful form of import:
from world.test import hello_world
The `from ... import ...` is very, very common as long as you want to get something with a longer
python path. It imports `hello_world` directly, so you can use it right away!
> py from world.test import hello_world ; hello_world(me)
Hello World!
Let's say your `test.py` module had a bunch of interesting functions. You could then import them
all one by one:
from world.test import hello_world, my_func, awesome_func
If there were _a lot_ of functions, you could instead just import `test` and get the function
from there when you need (without having to give the full `world.test` every time):
> from world import test ; test.hello_world(me
Hello World!
You can also _rename_ stuff you import. Say for example that the module you import to already
has a function `hello_world` but we also want to use the one from `world/test.py`:
from world.test import hello_world as test_hello_world
The form `from ... import ... as ...` renames the import.
> from world.test import hello_world as hw ; hw(me)
Hello World!
> Avoid renaming unless it's to avoid a name-collistion like above - you want to make things as
> easy to read as possible, and renaming adds another layer of potential confusion.
In [the basic intro to Python](./Beginner-Tutorial-Python-basic-introduction.md) we learned how to open the in-game
multi-line interpreter.
> py
Evennia Interactive Python mode
Python 3.7.1 (default, Oct 22 2018, 11:21:55)
[GCC 8.2.0] on Linux
[py mode - quit() to exit]
You now only need to import once to use the imported function over and over.
> from world.test import hello_world
> hello_world()
Hello World!
> hello_world()
Hello World!
> hello_world()
Hello World!
> quit()
Closing the Python console.
The same goes when writing code in a module - in most Python modules you will see a bunch of
imports at the top, resources that are then used by all code in that module.
## On classes and objects
Now that we know about imports, let look at a real Evennia module and try to understand it.
Open `mygame/typeclasses/objects.py` in your text editor of choice.
```python
"""
module docstring
"""
from evennia import DefaultObject
class Object(DefaultObject):
"""
class docstring
"""
pass
```
```{sidebar} Docstrings vs Comments
A docstring is not the same as a comment (created by `#`). A docstring is not ignored by Python but is an integral part of the thing it is documenting (the module and the class in this case).
```
The real file is much longer but we can ignore the multi-line strings (`""" ... """`). These serve
as documentation-strings, or _docstrings_ for the module (at the top) and the `class` below.
Below the module doc string we have the import. In this case we are importing a resource
from the core `evennia` library itself. We will dive into this later, for now we just treat this
as a black box.
Next we have a `class` named `Object`, which _inherits_ from `DefaultObject`. This class doesn't
actually do anything on its own, its only code (except the docstring) is `pass` which means,
well, to pass and don't do anything.
We will get back to this module in the [next lesson](./Beginner-Tutorial-Learning-Typeclasses.md). First we need to do a
little detour to understand what a 'class', an 'object' or 'instance' is. These are fundamental
things to understand before you can use Evennia efficiently.
```{sidebar} OOP
Classes, objects, instances and inheritance are fundamental to Python. This and some other concepts are often clumped together under the term Object-Oriented-Programming (OOP).
```
### Classes and instances
A 'class' can be seen as a 'template' for a 'type' of object. The class describes the basic functionality
of everyone of that class. For example, we could have a class `Monster` which has resources for moving itself
from room to room.
Open a new file `mygame/typeclasses/monsters.py`. Add the following simple class:
```python
class Monster:
key = "Monster"
def move_around(self):
print(f"{self.key} is moving!")
```
Above we have defined a `Monster` class with one variable `key` (that is, the name) and one
_method_ on it. A method is like a function except it sits "on" the class. It also always has
at least one argument (almost always written as `self` although you could in principle use
another name), which is a reference back to itself. So when we print `self.key` we are referring
back to the `key` on the class.
```{sidebar} Terms
- A `class` is a code template describing a 'type' of something
- An `object` is an `instance` of a `class`. Like using a mold to cast in soldiers, one class can be `instantiated` into any number of object-instances.
```
A class is just a template. Before it can be used, we must create an _instance_ of the class. If
`Monster` is a class, then an instance is Fluffy, the individual red dragon. You instantiate
by _calling_ the class, much like you would a function:
fluffy = Monster()
Let's try it in-game (we use multi-line mode, it's easier)
> py
> from typeclasses.monsters import Monster
> fluffy = Monster()
> fluffy.move_around()
Monster is moving!
We created an _instance_ of `Monster`, which we stored in the variable `fluffy`. We then
called the `move_around` method on fluffy to get the printout.
> Note how we _didn't_ call the method as `fluffy.move_around(self)`. While the `self` has to be
> there when defining the method, we _never_ add it explicitly when we call the method (Python
> will add the correct `self` for us automatically behind the scenes).
Let's create the sibling of Fluffy, Cuddly:
> cuddly = Monster()
> cuddly.move_around()
Monster is moving!
We now have two dragons and they'll hang around until with call `quit()` to exit this Python
instance. We can have them move as many times as we want. But no matter how many dragons we
create, they will all show the same printout since `key` is always fixed as "Monster".
Let's make the class a little more flexible:
```python
class Monster:
def __init__(self, key):
self.key = key
def move_around(self):
print(f"{self.key} is moving!")
```
The `__init__` is a special method that Python recognizes. If given, this handles extra arguments
when you instantiate a new Monster. We have it add an argument `key` that we store on `self`.
Now, for Evennia to see this code change, we need to reload the server. You can either do it this
way:
> quit()
Python Console is closing.
> reload
Or you can use a separate terminal and restart from outside the game:
```{sidebar} On reloading
Reloading with the python mode gets a little annoying since you need to redo everything after every reload. Just keep in mind that during regular development you will not be working this way. The in-game python mode is practical for quick fixes and experiments like this, but actual code is normally written externally, in python modules.
```
$ evennia reload (or restart)
Either way you'll need to go into `py` again:
> py
> from typeclasses.monsters import Monster
fluffy = Monster("Fluffy")
fluffy.move_around()
Fluffy is moving!
Now we passed `"Fluffy"` as an argument to the class. This went into `__init__` and set `self.key`, which we
later used to print with the right name! Again, note that we didn't include `self` when calling.
### What's so good about objects?
So far all we've seen a class do is to behave our first `hello_world` function but more complex. We
could just have made a function:
```python
def monster_move_around(key):
print(f"{key} is moving!")
```
The difference between the function and an instance of a class (the object), is that the
object retains _state_. Once you called the function it forgets everything about what you called
it with last time. The object, on the other hand, remembers changes:
> fluffy.key = "Cuddly"
> fluffy.move_around()
Cuddly is moving!
The `fluffy` object's `key` was changed to "Cuddly" for as long as it's around. This makes objects
extremely useful for representing and remembering collections of data - some of which can be other
objects in turn:
- A player character with all its stats
- A monster with HP
- A chest with a number of gold coins in it
- A room with other objects inside it
- 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/monsters.py` with another class:
```python
class Monster:
"""
This is a base class for Monster.
"""
def __init__(self, key):
self.key = key
def move_around(self):
print(f"{self.key} is moving!")
class Dragon(Monster):
"""
This is a dragon-specific monster.
"""
def move_around(self):
print(f"{self.key} flies through the air high above!")
def firebreath(self):
"""
Let our dragon breathe fire.
"""
print(f"{self.key} breathes fire!")
```
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 `Monster` 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.
```
Let's try out our new class. First `reload` the server and the do
> py
> from typeclasses.monsters 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.monsters 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.
## Summary
We have created our first dragons from classes. We have learned a little about how you _instantiate_ a class
into an _object_. We have seen some examples of _inheritance_ and we tested to _override_ a method in the parent
with one in the child class. We also used `super()` to good effect.
We have used pretty much raw Python so far. In the coming lessons we'll start to look at the extra bits that Evennia
provides. But first we need to learn just where to find everything.

View file

@ -1,259 +0,0 @@
# Searching for things
We have gone through how to create the various entities in Evennia. But creating something is of little use
if we cannot find and use it afterwards.
## Main search functions
The base tools are the `evennia.search_*` functions, such as `evennia.search_object`.
rose = evennia.search_object(key="rose")
acct = evennia.search_account(key="MyAccountName", email="foo@bar.com")
```{sidebar} Querysets
What is returned from the main search functions is actually a `queryset`. They can be treated like lists except that they can't modified in-place. We'll discuss querysets in the `next lesson` <Django-queries>`_.
```
Strings are always case-insensitive, so searching for `"rose"`, `"Rose"` or `"rOsE"` give the same results.
It's important to remember that what is returned from these search methods is a _listing_ of 0, one or more
elements - all the matches to your search. To get the first match:
rose = rose[0]
Often you really want all matches to the search parameters you specify. In other situations, having zero or
more than one match is a sign of a problem and you need to handle this case yourself.
the_one_ring = evennia.search_object(key="The one Ring")
if not the_one_ring:
# handle not finding the ring at all
elif len(the_one_ring) > 1:
# handle finding more than one ring
else:
# ok - exactly one ring found
the_one_ring = the_one_ring[0]
There are equivalent search functions for all the main resources. You can find a listing of them
[in the Search functions section](../../../Evennia-API.md) of the API frontpage.
## Searching using Object.search
On the `DefaultObject` is a `.search` method which we have already tried out when we made Commands. For
this to be used you must already have an object available:
rose = obj.search("rose")
The `.search` method wraps `evennia.search_object` and handles its output in various ways.
- By default it will always search for objects among those in `obj.location.contents` and `obj.contents` (that is,
things in obj's inventory or in the same room).
- It will always return exactly one match. If it found zero or more than one match, the return is `None`.
- On a no-match or multimatch, `.search` will automatically send an error message to `obj`.
So this method handles error messaging for you. A very common way to use it is in commands:
```python
from evennia import Command
class MyCommand(Command):
key = "findfoo"
def func(self):
foo = self.caller.search("foo")
if not foo:
return
```
Remember, `self.caller` is the one calling the command. This is usually a Character, which
inherits from `DefaultObject`! This (rather stupid) Command searches for an object named "foo" in
the same location. If it can't find it, `foo` will be `None`. The error has already been reported
to `self.caller` so we just abort with `return`.
You can use `.search` to find anything, not just stuff in the same room:
volcano = self.caller.search("Volcano", global=True)
If you only want to search for a specific list of things, you can do so too:
stone = self.caller.search("MyStone", candidates=[obj1, obj2, obj3, obj4])
This will only return a match if MyStone is one of the four provided candidate objects. This is quite powerful,
here's how you'd find something only in your inventory:
potion = self.caller.search("Healing potion", candidates=self.caller.contents)
You can also turn off the automatic error handling:
swords = self.caller.search("Sword", quiet=True)
With `quiet=True` the user will not be notified on zero or multi-match errors. Instead you are expected to handle this
yourself and what you get back is now a list of zero, one or more matches!
## What can be searched for
These are the main database entities one can search for:
- [Objects](../../../Components/Objects.md)
- [Accounts](../../../Components/Accounts.md)
- [Scripts](../../../Components/Scripts.md),
- [Channels](../../../Components/Channels.md),
- [Messages](../../../Components/Msg.md)
- [Help Entries](../../../Components/Help-System.md).
Most of the time you'll likely spend your time searching for Objects and the occasional Accounts.
So to find an entity, what can be searched for?
### Search by key
The `key` is the name of the entity. Searching for this is always case-insensitive.
### Search by aliases
Objects and Accounts can have any number of aliases. When searching for `key` these will searched too,
you can't easily search only for aliases.
rose.aliases.add("flower")
If the above `rose` has a `key` `"Rose"`, it can now also be found by searching for `flower`. In-game
you can assign new aliases to things with the `alias` command.
### Search by location
Only Objects (things inheriting from `evennia.DefaultObject`) has a location. This is usually a room.
The `Object.search` method will automatically limit it search by location, but it also works for the
general search function. If we assume `room` is a particular Room instance,
chest = evennia.search_object("Treasure chest", location=room)
### Search by Tags
Think of a [Tag](../../../Components/Tags.md) as the label the airport puts on your luggage when flying.
Everyone going on the same plane gets a tag grouping them together so the airport can know what should
go to which plane. Entities in Evennia can be grouped in the same way. Any number of tags can be attached
to each object.
rose.tags.add("flowers")
daffodil.tags.add("flowers")
tulip.tags.add("flowers")
You can now find all flowers using the `search_tag` function:
all_flowers = evennia.search_tag("flowers")
Tags can also have categories. By default this category is `None` which is also considered a category.
silmarillion.tags.add("fantasy", category="books")
ice_and_fire.tags.add("fantasy", category="books")
mona_lisa_overdrive.tags.add("cyberpunk", category="books")
Note that if you specify the tag you _must_ also include its category, otherwise that category
will be `None` and find no matches.
all_fantasy_books = evennia.search_tag("fantasy") # no matches!
all_fantasy_books = evennia.search_tag("fantasy", category="books")
Only the second line above returns the two fantasy books. If we specify a category however,
we can get all tagged entities within that category:
all_books = evennia.search_tag(category="books")
This gets all three books.
### Search by Attribute
We can also search by the [Attributes](../../../Components/Attributes.md) associated with entities.
For example, let's give our rose thorns:
rose.db.has_thorns = True
wines.db.has_thorns = True
daffodil.db.has_thorns = False
Now we can find things attribute and the value we want it to have:
is_ouch = evennia.search_object_attribute("has_thorns", True)
This returns the rose and the wines.
> Searching by Attribute can be very practical. But if you plan to do a search very often, searching
> by-tag is generally faster.
### Search by Typeclass
Sometimes it's useful to find all objects of a specific Typeclass. All of Evennia's search tools support this.
all_roses = evennia.search_object(typeclass="typeclasses.flowers.Rose")
If you have the `Rose` class already imported you can also pass it directly:
all_roses = evennia.search_object(typeclass=Rose)
You can also search using the typeclass itself:
all_roses = Rose.objects.all()
This last way of searching is a simple form of a Django _query_. This is a way to express SQL queries using
Python.
### Search by dbref
The database id or `#dbref` is unique and never-reused within each database table. In search methods you can
replace the search for `key` with the dbref to search for. This must be written as a string `#dbref`:
the_answer = self.caller.search("#42")
eightball = evennia.search_object("#8")
Since `#dbref` is always unique, this search is always global.
```{warning} Relying on #dbrefs
You may be used to using #dbrefs a lot from other codebases. It is however considered
`bad practice` in Evennia to rely on hard-coded #dbrefs. It makes your code hard to maintain
and tied to the exact layout of the database. In 99% of cases you should pass the actual objects
around and search by key/tags/attribute instead.
```
## Finding objects relative each other
Let's consider a `chest` with a `coin` inside it. The chests stand in a room `dungeon`. In the dungeon is also
a `door`. This is an exit leading outside.
- `coin.location` is `chest`.
- `chest.location` is `dungeon`.
- `door.location` is `dungeon`.
- `room.location` is `None` since it's not inside something else.
One can use this to find what is inside what. For example, `coin.location.location` is the `room`.
We can also find what is inside each object. This is a list of things.
- `room.contents` is `[chest, door]`
- `chest.contents` is `[coin]`
- `coin.contents` is `[]`, the empty list since there's nothing 'inside' the coin.
- `door.contents` is `[]` too.
A convenient helper is `.contents_get` - this allows to restrict what is returned:
- `room.contents_get(exclude=chest)` - this returns everything in the room except the chest (maybe it's hidden?)
There is a special property for finding exits:
- `room.exits` is `[door]`
- `coin.exits` is `[]` (same for all the other objects)
There is a property `.destination` which is only used by exits:
- `door.destination` is `outside` (or wherever the door leads)
- `room.destination` is `None` (same for all the other non-exit objects)
## Summary
Knowing how to find things is important and the tools from this section will serve you well. For most of your needs
these tools will be all you need ...
... but not always. In the next lesson we will dive further into more complex searching when we look at
Django queries and querysets in earnest.

View file

@ -1,123 +0,0 @@
# The Tutorial World
The *Tutorial World* is a small and functioning MUD-style game world shipped with Evennia.
It's a small showcase of what is possible. It can also be useful for those who have an easier
time learning by deconstructing existing code.
Stand in the Limbo room and install it with
batchcommand tutorial_world.build
What this does is to run the build script
[evennia/contrib/tutorial_world/build.ev](github:evennia/contrib/tutorial_world/build.ev).
This is pretty much just a list of build-commands executed in sequence by the `batchcommand` command.
Wait for the building to complete and don't run it twice.
> After having run the batchcommand, the `intro` command also becomes available in Limbo. Try it out to
> for in-game help and to get an example of [EvMenu](../../../Components/EvMenu.md), Evennia's in-built
> menu generation system!
The game consists of a single-player quest and has some 20 rooms that you can explore as you seek
to discover the whereabouts of a mythical weapon.
A new exit should have appeared named _Tutorial_. Enter by writing `tutorial`.
You will automatically `quell` when you enter (and `unquell` when you leave), so you can play the way it was intended.
Both if you are triumphant or if you use the `give up` command you will eventually end up back in Limbo.
```{important}
Only LOSERS and QUITTERS use the `give up` command.
```
## Gameplay
![the castle off the moor](https://images-wixmp-ed30a86b8c4ca887773594c2.wixmp.com/f/22916c25-6299-453d-a221-446ec839f567/da2pmzu-46d63c6d-9cdc-41dd-87d6-1106db5a5e1a.jpg/v1/fill/w_600,h_849,q_75,strp/the_castle_off_the_moor_by_griatch_art_da2pmzu-fullview.jpg?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1cm46YXBwOiIsImlzcyI6InVybjphcHA6Iiwib2JqIjpbW3siaGVpZ2h0IjoiPD04NDkiLCJwYXRoIjoiXC9mXC8yMjkxNmMyNS02Mjk5LTQ1M2QtYTIyMS00NDZlYzgzOWY1NjdcL2RhMnBtenUtNDZkNjNjNmQtOWNkYy00MWRkLTg3ZDYtMTEwNmRiNWE1ZTFhLmpwZyIsIndpZHRoIjoiPD02MDAifV1dLCJhdWQiOlsidXJuOnNlcnZpY2U6aW1hZ2Uub3BlcmF0aW9ucyJdfQ.omuS3D1RmFiZCy9OSXiIita-HxVGrBok3_7asq0rflw)
*To get into the mood of this miniature quest, imagine you are an adventurer out to find fame and
fortune. You have heard rumours of an old castle ruin by the coast. In its depth a warrior princess
was buried together with her powerful magical weapon - a valuable prize, if it's true. Of course
this is a chance to adventure that you cannot turn down!*
*You reach the ocean in the midst of a raging thunderstorm. With wind and rain screaming in your
face you stand where the moor meets the sea along a high, rocky coast ...*
---
### Gameplay hints
- Use the command `tutorial` to get code insight behind the scenes of every room.
- Look at everything. While a demo, the Tutorial World is not necessarily trivial to solve - it depends
on your experience with text-based adventure games. Just remember that everything can be solved or bypassed.
- Some objects are interactive in more than one way. Use the normal `help` command to get a feel for
which commands are available at any given time.
- In order to fight, you need to first find some type of weapon.
- *slash* is a normal attack
- *stab* launches an attack that makes more damage but has a lower chance to hit.
- *defend* will lower the chance to taking damage on your enemy's next attack.
- Some things _cannot_ be hurt by mundane weapons. In that case it's OK to run away. Expect
to be chased though.
- Being defeated is a part of the experience. You can't actually die, but getting knocked out
means being left in the dark ...
## Once you are done (or had enough)
Afterwards you'll either have conquered the old ruin and returned in glory and triumph ... or
you returned limping and whimpering from the challenge by using the `give up` command.
Either way you should now be back in Limbo, able to reflect on the experience.
Some features exemplified by the tutorial world:
- Rooms with custom ability to show details (like looking at the wall in the dark room)
- Hidden or impassable exits until you fulfilled some criterion
- Objects with multiple custom interactions (like swords, the well, the obelisk ...)
- Large-area rooms (that bridge is actually only one room!)
- Outdoor weather rooms with weather (the rain pummeling you)
- Dark room, needing light source to reveal itself (the burning splinter even burns out after a while)
- Puzzle object (the wines in the dark cell; hope you didn't get stuck!)
- Multi-room puzzle (the obelisk and the crypt)
- Aggressive mobile with roam, pursue and battle state-engine AI (quite deadly until you find the right weapon)
- Weapons, also used by mobs (most are admittedly not that useful against the big baddie)
- Simple combat system with attack/defend commands (teleporting on-defeat)
- Object spawning (the weapons in the barrel and the final weapoon is actually randomized)
- Teleporter trap rooms (if you fail the obelisk puzzle)
```{sidebar} Extra Credit
If you have previous programming experience (or after you have gone
through this Starter tutorial) it may be instructive to dig a little deeper into the Tutorial-world
code to learn how it achieves what it does. The code is heavily documented.
You can find all the code in [evennia/contrib/tutorials/tutorial_world](../../../api/evennia.contrib.tutorials.tutorial_world.md).
The build-script is [here](github:evennia/contrib/tutorials/tutorial_world/build.ev).
When reading the code, remember that the Tutorial World was designed to install easily and to not permanently modify
the rest of the game. It therefore makes sure to only use temporary solutions and to clean up after itself. This is
not something you will often need to worry about when making your own game.
```
Quite a lot of stuff crammed in such a small area!
## Uninstall the tutorial world
Once are done playing with the tutorial world, let's uninstall it.
Uninstalling the tutorial world basically means deleting all the rooms and objects it consists of.
Make sure you are back in Limbo, then
find tut#01
find tut#16
This should locate the first and last rooms created by `build.ev` - *Intro* and *Outro*. If you
installed normally, everything created between these two numbers should be part of the tutorial.
Note their #dbref numbers, for example 5 and 80. Next we just delete all objects in that range:
del 5-80
You will see some errors since some objects are auto-deleted and so cannot be found when the delete
mechanism gets to them. That's fine. You should have removed the tutorial completely once the
command finishes.
Even if the game-style of the Tutorial-world was not similar to the one you are interested in, it
should hopefully have given you a little taste of some of the possibilities of Evennia. Now we'll
move on with how to access this power through code.