mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Further updates
This commit is contained in:
parent
830f793aa4
commit
a515ececff
11 changed files with 856 additions and 540 deletions
|
|
@ -1,109 +0,0 @@
|
|||
# Adding Object Typeclass Tutorial
|
||||
|
||||
Evennia comes with a few very basic classes of in-game entities:
|
||||
|
||||
DefaultObject
|
||||
|
|
||||
DefaultCharacter
|
||||
DefaultRoom
|
||||
DefaultExit
|
||||
DefaultChannel
|
||||
|
||||
When you create a new Evennia game (with for example `evennia --init mygame`) Evennia will
|
||||
automatically create empty child classes `Object`, `Character`, `Room` and `Exit` respectively. They
|
||||
are found `mygame/typeclasses/objects.py`, `mygame/typeclasses/rooms.py` etc.
|
||||
|
||||
> Technically these are all [Typeclassed](../../Component/Typeclasses), which can be ignored for now. In
|
||||
> `mygame/typeclasses` are also base typeclasses for out-of-character things, notably
|
||||
> [Channels](../../Component/Communications), [Accounts](../../Component/Accounts) and [Scripts](../../Component/Scripts). We don't cover those in
|
||||
> this tutorial.
|
||||
|
||||
For your own game you will most likely want to expand on these very simple beginnings. It's normal
|
||||
to want your Characters to have various attributes, for example. Maybe Rooms should hold extra
|
||||
information or even *all* Objects in your game should have properties not included in basic Evennia.
|
||||
|
||||
## Change Default Rooms, Exits, Character Typeclass
|
||||
|
||||
This is the simplest case.
|
||||
|
||||
The default build commands of a new Evennia game is set up to use the `Room`, `Exit` and `Character`
|
||||
classes found in the same-named modules under `mygame/typeclasses/`. By default these are empty and
|
||||
just implements the default parents from the Evennia library (`DefaultRoom`etc). Just add the
|
||||
changes you want to these classes and run `@reload` to add your new functionality.
|
||||
|
||||
## Create a new type of object
|
||||
|
||||
Say you want to create a new "Heavy" object-type that characters should not have the ability to pick
|
||||
up.
|
||||
|
||||
1. Edit `mygame/typeclasses/objects.py` (you could also create a new module there, named something
|
||||
like `heavy.py`, that's up to how you want to organize things).
|
||||
1. Create a new class inheriting at any distance from `DefaultObject`. It could look something like
|
||||
this:
|
||||
```python
|
||||
# end of file mygame/typeclasses/objects.py
|
||||
from evennia import DefaultObject
|
||||
|
||||
class Heavy(DefaultObject):
|
||||
"Heavy object"
|
||||
def at_object_creation(self):
|
||||
"Called whenever a new object is created"
|
||||
# lock the object down by default
|
||||
self.locks.add("get:false()")
|
||||
# the default "get" command looks for this Attribute in order
|
||||
# to return a customized error message (we just happen to know
|
||||
# this, you'd have to look at the code of the 'get' command to
|
||||
# find out).
|
||||
self.db.get_err_msg = "This is too heavy to pick up."
|
||||
```
|
||||
1. Once you are done, log into the game with a build-capable account and do `@create/drop
|
||||
rock:objects.Heavy` to drop a new heavy "rock" object in your location. Next try to pick it up
|
||||
(`@quell` yourself first if you are a superuser). If you get errors, look at your log files where
|
||||
you will find the traceback. The most common error is that you have some sort of syntax error in
|
||||
your class.
|
||||
|
||||
Note that the [Locks](../../Component/Locks) and [Attribute](../../Component/Attributes) which are set in the typeclass could just
|
||||
as well have been set using commands in-game, so this is a *very* simple example.
|
||||
|
||||
## Storing data on initialization
|
||||
|
||||
The `at_object_creation` is only called once, when the object is first created. This makes it ideal
|
||||
for database-bound things like [Attributes](../../Component/Attributes). But sometimes you want to create temporary
|
||||
properties (things that are not to be stored in the database but still always exist every time the
|
||||
object is created). Such properties can be initialized in the `at_init` method on the object.
|
||||
`at_init` is called every time the object is loaded into memory.
|
||||
|
||||
> Note: It's usually pointless and wasteful to assign database data in `at_init`, since this will
|
||||
> hit the database with the same value over and over. Put those in `at_object_creation` instead.
|
||||
|
||||
You are wise to use `ndb` (non-database Attributes) to store these non-persistent properties, since
|
||||
ndb-properties are protected against being cached out in various ways and also allows you to list
|
||||
them using various in-game tools:
|
||||
|
||||
```python
|
||||
def at_init(self):
|
||||
self.ndb.counter = 0
|
||||
self.ndb.mylist = []
|
||||
```
|
||||
|
||||
> Note: As mentioned in the [Typeclasses](../../Component/Typeclasses) documentation, `at_init` replaces the use of
|
||||
> the standard `__init__` method of typeclasses due to how the latter may be called in situations
|
||||
> other than you'd expect. So use `at_init` where you would normally use `__init__`.
|
||||
|
||||
|
||||
## Updating existing objects
|
||||
|
||||
If you already have some `Heavy` objects created and you add a new `Attribute` in
|
||||
`at_object_creation`, you will find that those existing objects will not have this Attribute. This
|
||||
is not so strange, since `at_object_creation` is only called once, it will not be called again just
|
||||
because you update it. You need to update existing objects manually.
|
||||
|
||||
If the number of objects is limited, you can use `@typeclass/force/reload objectname` to force a
|
||||
re-load of the `at_object_creation` method (only) on the object. This case is common enough that
|
||||
there is an alias `@update objectname` you can use to get the same effect. If there are multiple
|
||||
objects you can use `@py` to loop over the objects you need:
|
||||
|
||||
```
|
||||
@py from typeclasses.objects import Heavy; [obj.at_object_creation() for obj in Heavy.objects.all()]
|
||||
|
||||
```
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
# Coding Introduction
|
||||
|
||||
|
||||
Evennia allows for a lot of freedom when designing your game - but to code efficiently you still
|
||||
need to adopt some best practices as well as find a good place to start to learn.
|
||||
|
||||
Here are some pointers to get you going.
|
||||
|
||||
### Python
|
||||
|
||||
Evennia is developed using Python. Even if you are more of a designer than a coder, it is wise to
|
||||
learn how to read and understand basic Python code. If you are new to Python, or need a refresher,
|
||||
take a look at our two-part [Python introduction](Part1/Python-basic-introduction).
|
||||
|
||||
### Explore Evennia interactively
|
||||
|
||||
When new to Evennia it can be hard to find things or figure out what is available. Evennia offers a
|
||||
special interactive python shell that allows you to experiment and try out things. It's recommended
|
||||
to use [ipython](http://ipython.org/) for this since the vanilla python prompt is very limited. Here
|
||||
are some simple commands to get started:
|
||||
|
||||
# [open a new console/terminal]
|
||||
# [activate your evennia virtualenv in this console/terminal]
|
||||
pip install ipython # [only needed the first time]
|
||||
cd mygame
|
||||
evennia shell
|
||||
|
||||
This will open an Evennia-aware python shell (using ipython). From within this shell, try
|
||||
|
||||
import evennia
|
||||
evennia.<TAB>
|
||||
|
||||
That is, enter `evennia.` and press the `<TAB>` key. This will show you all the resources made
|
||||
available at the top level of Evennia's "flat API". See the [flat API](../../Evennia-API) page for more
|
||||
info on how to explore it efficiently.
|
||||
|
||||
You can complement your exploration by peeking at the sections of the much more detailed [Developer
|
||||
Central](Developer-Central). The [Tutorials](Tutorials) section also contains a growing collection
|
||||
of system- or implementation-specific help.
|
||||
|
||||
### Use a python syntax checker
|
||||
|
||||
Evennia works by importing your own modules and running them as part of the server. Whereas Evennia
|
||||
should just gracefully tell you what errors it finds, it can nevertheless be a good idea for you to
|
||||
check your code for simple syntax errors *before* you load it into the running server. There are
|
||||
many python syntax checkers out there. A fast and easy one is
|
||||
[pyflakes](https://pypi.python.org/pypi/pyflakes), a more verbose one is
|
||||
[pylint](http://www.pylint.org/). You can also check so that your code looks up to snuff using
|
||||
[pep8](https://pypi.python.org/pypi/pep8). Even with a syntax checker you will not be able to catch
|
||||
every possible problem - some bugs or problems will only appear when you actually run the code. But
|
||||
using such a checker can be a good start to weed out the simple problems.
|
||||
|
||||
### Plan before you code
|
||||
|
||||
Before you start coding away at your dream game, take a look at our [Game Planning](Game-Planning)
|
||||
page. It might hopefully help you avoid some common pitfalls and time sinks.
|
||||
|
||||
### Code in your game folder, not in the evennia/ repository
|
||||
|
||||
As part of the Evennia setup you will create a game folder to host your game code. This is your
|
||||
home. You should *never* need to modify anything in the `evennia` library (anything you download
|
||||
from us, really). You import useful functionality from here and if you see code you like, copy&paste
|
||||
it out into your game folder and edit it there.
|
||||
|
||||
If you find that Evennia doesn't support some functionality you need, make a [Feature
|
||||
Request](feature-request) about it. Same goes for [bugs][bug]. If you add features or fix bugs
|
||||
yourself, please consider [Contributing](../../Contributing) your changes upstream!
|
||||
|
||||
### Learn to read tracebacks
|
||||
|
||||
Python is very good at reporting when and where things go wrong. A *traceback* shows everything you
|
||||
need to know about crashing code. The text can be pretty long, but you usually are only interested
|
||||
in the last bit, where it says what the error is and at which module and line number it happened -
|
||||
armed with this info you can resolve most problems.
|
||||
|
||||
Evennia will usually not show the full traceback in-game though. Instead the server outputs errors
|
||||
to the terminal/console from which you started Evennia in the first place. If you want more to show
|
||||
in-game you can add `IN_GAME_ERRORS = True` to your settings file. This will echo most (but not all)
|
||||
tracebacks both in-game as well as to the terminal/console. This is a potential security problem
|
||||
though, so don't keep this active when your game goes into production.
|
||||
|
||||
> A common confusing error is finding that objects in-game are suddenly of the type `DefaultObject`
|
||||
rather than your custom typeclass. This happens when you introduce a critical Syntax error to the
|
||||
module holding your custom class. Since such a module is not valid Python, Evennia can't load it at
|
||||
all. Instead of crashing, Evennia will then print the full traceback to the terminal/console and
|
||||
temporarily fall back to the safe `DefaultObject` until you fix the problem and reload.
|
||||
|
||||
### Docs are here to help you
|
||||
|
||||
Some people find reading documentation extremely dull and shun it out of principle. That's your
|
||||
call, but reading docs really *does* help you, promise! Evennia's documentation is pretty thorough
|
||||
and knowing what is possible can often give you a lot of new cool game ideas. That said, if you
|
||||
can't find the answer in the docs, don't be shy to ask questions! The [discussion
|
||||
group](https://sites.google.com/site/evenniaserver/discussions) and the [irc
|
||||
chat](http://webchat.freenode.net/?channels=evennia) are also there for you.
|
||||
|
||||
### The most important point
|
||||
|
||||
And finally, of course, have fun!
|
||||
|
||||
[feature-request]: (https://github.com/evennia/evennia/issues/new?title=Feature+Request%3a+%3Cdescriptive+title+here%3E&body=%23%23%23%23+Description+of+the+suggested+feature+and+how+it+is+supposed+to+work+for+the+admin%2fend+user%3a%0D%0A%0D%0A%0D%0A%23%23%23%23+A+list+of+arguments+for+why+you+think+this+new+feature+should+be+included+in+Evennia%3a%0D%0A%0D%0A1.%0D%0A2.%0D%0A%0D%0A%23%23%23%23+Extra+information%2c+such+as+requirements+or+ideas+on+implementation%3a%0D%0A%0D%0A
|
||||
[bug](https://github.com/evennia/evennia/issues/new?title=Bug%3a+%3Cdescriptive+title+here%3E&body=%23%23%23%23+Steps+to+reproduce+the+issue%3a%0D%0A%0D%0A1.+%0D%0A2.+%0D%0A3.+%0D%0A%0D%0A%23%23%23%23+What+I+expect+to+see+and+what+I+actually+see+%28tracebacks%2c+error+messages+etc%29%3a%0D%0A%0D%0A%0D%0A%0D%0A%23%23%23%23+Extra+information%2c+such+as+Evennia+revision%2frepo%2fbranch%2c+operating+system+and+ideas+for+how+to+solve%3a%0D%0A%0D%0A)
|
||||
|
|
@ -10,7 +10,7 @@ Started](Getting-Started) instructions. You should have initialized a new game f
|
|||
`evennia --init foldername` command. We will in the following assume this folder is called
|
||||
"mygame".
|
||||
|
||||
It might be a good idea to eye through the brief [Coding Introduction](Coding-Introduction) too
|
||||
It might be a good idea to eye through the brief [Coding Introduction](../../Coding/Coding-Introduction) too
|
||||
(especially the recommendations in the section about the evennia "flat" API and about using `evennia
|
||||
shell` will help you here and in the future).
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
# Adding Command Tutorial
|
||||
# Our own commands
|
||||
|
||||
[prev lesson](Python-classes-and-objects) | [next lesson]()
|
||||
[prev lesson](Python-classes-and-objects) | [next lesson](More-on-Commands)
|
||||
|
||||
In this lesson we'll learn how to create our own Evennia _Commands_. If you are new to Python you'll
|
||||
also learn some more basics about how to manipulate strings and get information out of Evennia.
|
||||
|
||||
A Command is something that handles the input from a user and causes a result to happen.
|
||||
An example is `look`, which examines your current location and tells how it looks like and
|
||||
|
|
@ -10,16 +13,17 @@ In Evennia, a Command is a Python _class_. If you are unsure about what a class
|
|||
previous lesson. A Command inherits from `evennia.Command` or from one of the alternative command-
|
||||
classes, such as `MuxCommand` which is what most default commands use.
|
||||
|
||||
All Commands are in turn grouped in another class called a _Command Set_. Think of a Command set
|
||||
All Commands are in turn grouped in another class called a _Command Set_. Think of a Command Set
|
||||
as a bag holding many different commands. One CmdSet could for example hold all commands for
|
||||
combat, another for building etc.
|
||||
combat, another for building etc. By default, Evennia groups all character-commands into one
|
||||
big cmdset.
|
||||
|
||||
Command-Sets are then associated with objects. Doing so makes the commands in that cmdset available
|
||||
to the object. So, to summarize:
|
||||
|
||||
- Commands are classes
|
||||
- A group of Commands is stored in a CmdSet
|
||||
- Putting a CmdSet on an object makes all commands in it available to the object
|
||||
- CmdSets are stored on objects - this defines which commands are available to that object.
|
||||
|
||||
## Creating a custom command
|
||||
|
||||
|
|
@ -373,271 +377,14 @@ you could try to hit it (if you dare):
|
|||
|
||||
You won't see the second string. Only Smaug sees that (and is not amused).
|
||||
|
||||
## More advanced parsing
|
||||
|
||||
Let's expand our simple `hit` command to accept a little more complex input:
|
||||
## Summary
|
||||
|
||||
hit <target> [[with] <weapon>]
|
||||
|
||||
That is, we want to support all of these forms
|
||||
In this lesson we learned how to create our own Command, add it to a CmdSet and then ourselves.
|
||||
We also upset a dragon.
|
||||
|
||||
hit target
|
||||
hit target weapon
|
||||
hit target with weapon
|
||||
|
||||
If you don't specify a weapon you'll use your fists. It's also nice to be able to skip "with" if
|
||||
you are in a hurry. Time to modify `mygame/commands/mycommands.py` again. Let us break out the parsing
|
||||
a little, in a new method `parse`:
|
||||
In the next lesson we'll learn how to hit Smaug with different weapons. We'll also
|
||||
get into how we replace and extend Evennia's default Commands.
|
||||
|
||||
|
||||
```python
|
||||
#...
|
||||
|
||||
class CmdHit(Command):
|
||||
"""
|
||||
Hit a target.
|
||||
|
||||
Usage:
|
||||
hit <target>
|
||||
|
||||
"""
|
||||
key = "hit"
|
||||
|
||||
def parse(self):
|
||||
self.args = self.args.strip()
|
||||
target, *weapon = self.args.split(" with ", 1)
|
||||
if not weapon:
|
||||
target, *weapon = target.split(" ", 1)
|
||||
self.target = target.strip()
|
||||
if weapon:
|
||||
self.weapon = weapon.strip()
|
||||
else:
|
||||
self.weapon = ""
|
||||
|
||||
def func(self):
|
||||
if not self.args:
|
||||
self.caller.msg("Who do you want to hit?")
|
||||
return
|
||||
# get the target for the hit
|
||||
target = self.caller.search(self.target)
|
||||
if not target:
|
||||
return
|
||||
# get and handle the weapon
|
||||
weapon = None
|
||||
if self.weapon:
|
||||
weapon = self.caller.search(self.weapon)
|
||||
if weapon:
|
||||
weaponstr = f"{weapon.key}"
|
||||
else:
|
||||
weaponstr = "bare fists"
|
||||
|
||||
self.caller.msg(f"You hit {target.key} with {weaponstr}!")
|
||||
target.msg(f"You got hit by {self.caller.key} with {weaponstr}!")
|
||||
# ...
|
||||
|
||||
```
|
||||
|
||||
The `parse` method is called before `func` and has access to all the same on-command variables as in `func`. Using
|
||||
`parse` not only makes things a little easier to read, it also means you can easily let other Commands _inherit_
|
||||
your parsing - if you wanted some other Command to also understand input on the form `<arg> with <arg>` you'd inherit
|
||||
from this class and just implement the `func` needed for that command without implementing `parse` anew.
|
||||
|
||||
```sidebar:: Tuples and Lists
|
||||
|
||||
- A `list` is written as `[a, b, c, d, ...]`. You can add and grow/shrink a list after it was first created.
|
||||
- A `tuple` is written as `(a, b, c, d, ...)`. A tuple cannot be modified once it is created.
|
||||
|
||||
```
|
||||
- **Line 14** - We do the stripping of `self.args` once and for all here. We also store the stripped version back
|
||||
into `self.args`, overwriting it. So there is no way to get back the non-stripped version from here on, which is fine
|
||||
for this command.
|
||||
- **Line 15** - This makes use of the `.split` method of strings. `.split` will, well, split the string by some criterion.
|
||||
`.split(" with ", 1)` means "split the string once, around the substring `" with "` if it exists". The result
|
||||
of this split is a _list_. Just how that list looks depends on the string we are trying to split:
|
||||
1. If we entered just `hit smaug`, we'd be splitting just `"smaug"` which would give the result `["smaug"]`.
|
||||
2. `hit smaug sword` gives `["smaug sword"]`
|
||||
3. `hit smaug with sword` gives `["smaug", "sword"]`
|
||||
|
||||
So we get a list of 1 or 2 elements. We assign it to two variables like this, `target, *weapon = `. That
|
||||
asterisk in `*weapon` is a nifty trick - it will automatically become a list of _0 or more_ values. It sorts of
|
||||
"soaks" up everything left over.
|
||||
1. `target` becomes `"smaug"` and `weapon` becomes `[]`
|
||||
2. `target` becomes `"smaug sword"` and `weapon` becomes `[]`
|
||||
3. `target` becomes `"smaug"` and `weapon` becomes `sword`
|
||||
- **Lines 16-17** - In this `if` condition we check if `weapon` is falsy (that is, the empty list). This can happen
|
||||
under two conditions (from the example above):
|
||||
1. `target` is simply `smaug`
|
||||
2. `target` is `smaug sword`
|
||||
|
||||
To separate these cases we split `target` once again, this time by empty space `" "`. Again we store the
|
||||
result back with `target, *weapon =`. The result will be one of the following:
|
||||
1. `target` remains `smaug` and `weapon` remains `[]`
|
||||
2. `target` becomes `smaug` and `weapon` becomes `sword`
|
||||
- **Lines 18-22** - We now store `target` and `weapon` into `self.target` and `self.weapon`. We must do this in order
|
||||
for these local variables to made available in `func` later. Note how we need to check so `weapon` is not falsy
|
||||
before running `strip()` on it. This is because we know that if it's falsy, it's an empty list `[]` and lists
|
||||
don't have the `.strip()` method on them (so if we tried to use it, we'd get an error).
|
||||
|
||||
Now onto the `func` method. The main difference is we now have `self.target` and `self.weapon` available for
|
||||
convenient use.
|
||||
- **Lines 29 and 35** - We make use of the previously parsed search terms for the target and weapon to find the
|
||||
respective resource.
|
||||
- **Lines 34-39** - Since the weapon is optional, we need to supply a default (use our fists!) if it's not set. We
|
||||
use this to create a `weaponstr` that is different depending on if we have a weapon or not.
|
||||
- **Lines 41-42** - We merge the `weaponstr` with our attack text.
|
||||
|
||||
Let's try it out!
|
||||
|
||||
> reload
|
||||
> hit smaug with sword
|
||||
Could not find 'sword'.
|
||||
You hit smaug with bare fists!
|
||||
|
||||
Oops, our `self.caller.search(self.weapon)` is telling us that it found no sword. Since we are not `return`ing
|
||||
in this situation (like we do if failing to find `target`) we still continue fighting with our bare hands.
|
||||
This won't do. Let's make ourselves a sword.
|
||||
|
||||
> create sword
|
||||
|
||||
Since we didn't specify `/drop`, the sword will end up in our inventory and can seen with the `i` or
|
||||
`inventory` command. The `.search` helper will still find it there. There is no need to reload to see this
|
||||
change (no code changed, only stuff in the database).
|
||||
|
||||
> hit smaug with sword
|
||||
You hit smaug with sword!
|
||||
|
||||
|
||||
## Adding the Command to a default Cmdset
|
||||
|
||||
|
||||
For now, let's drop MyCmdSet:
|
||||
|
||||
> py self.cmdset.remove("commands.mycommands.MyCmdSet")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
The command is not available to use until it is part of a [Command Set](../../../Component/Command-Sets). In this
|
||||
example we will go the easiest route and add it to the default Character commandset that already
|
||||
exists.
|
||||
|
||||
1. Edit `mygame/commands/default_cmdsets.py`
|
||||
1. Import your new command with `from commands.command import CmdEcho`.
|
||||
1. Add a line `self.add(CmdEcho())` to `CharacterCmdSet`, in the `at_cmdset_creation` method (the
|
||||
template tells you where).
|
||||
|
||||
This is approximately how it should look at this point:
|
||||
|
||||
```python
|
||||
# file mygame/commands/default_cmdsets.py
|
||||
#[...]
|
||||
from commands.command import CmdEcho
|
||||
#[...]
|
||||
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||
|
||||
key = "DefaultCharacter"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
|
||||
# this first adds all default commands
|
||||
super(DefaultSet, self).at_cmdset_creation()
|
||||
|
||||
# all commands added after this point will extend or
|
||||
# overwrite the default commands.
|
||||
self.add(CmdEcho())
|
||||
```
|
||||
|
||||
Next, run the `@reload` command. You should now be able to use your new `echo` command from inside
|
||||
the game. Use `help echo` to see the documentation for the command.
|
||||
|
||||
If you have trouble, make sure to check the log for error messages (probably due to syntax errors in
|
||||
your command definition).
|
||||
|
||||
> Note: Typing `echotest` will also work. It will be handled as the command `echo` directly followed
|
||||
by
|
||||
its argument `test` (which will end up in `self.args). To change this behavior, you can add the
|
||||
`arg_regex` property alongside `key`, `help_category` etc. [See the arg_regex
|
||||
documentation](Commands#on-arg_regex) for more info.
|
||||
|
||||
If you want to overload existing default commands (such as `look` or `get`), just add your new
|
||||
command with the same key as the old one - it will then replace it. Just remember that you must use
|
||||
`@reload` to see any changes.
|
||||
|
||||
See [Commands](../../../Component/Commands) for many more details and possibilities when defining Commands and using
|
||||
Cmdsets in various ways.
|
||||
|
||||
|
||||
## Adding the command to specific object types
|
||||
|
||||
Adding your Command to the `CharacterCmdSet` is just one easy exapmple. The cmdset system is very
|
||||
generic. You can create your own cmdsets (let's say in a module `mycmdsets.py`) and add them to
|
||||
objects as you please (how to control their merging is described in detail in the [Command Set
|
||||
documentation](Command-Sets)).
|
||||
|
||||
```python
|
||||
# file mygame/commands/mycmdsets.py
|
||||
#[...]
|
||||
from commands.command import CmdEcho
|
||||
from evennia import CmdSet
|
||||
#[...]
|
||||
class MyCmdSet(CmdSet):
|
||||
|
||||
key = "MyCmdSet"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdEcho())
|
||||
```
|
||||
Now you just need to add this to an object. To test things (as superuser) you can do
|
||||
|
||||
@py self.cmdset.add("mycmdsets.MyCmdSet")
|
||||
|
||||
This will add this cmdset (along with its echo command) to yourself so you can test it. Note that
|
||||
you cannot add a single Command to an object on its own, it must be part of a CommandSet in order to
|
||||
do so.
|
||||
|
||||
The Command you added is not there permanently at this point. If you do a `@reload` the merger will
|
||||
be gone. You *could* add the `permanent=True` keyword to the `cmdset.add` call. This will however
|
||||
only make the new merged cmdset permanent on that *single* object. Often you want *all* objects of
|
||||
this particular class to have this cmdset.
|
||||
|
||||
To make sure all new created objects get your new merged set, put the `cmdset.add` call in your
|
||||
custom [Typeclasses](../../../Component/Typeclasses)' `at_object_creation` method:
|
||||
|
||||
```python
|
||||
# e.g. in mygame/typeclasses/objects.py
|
||||
|
||||
from evennia import DefaultObject
|
||||
class MyObject(DefaultObject):
|
||||
|
||||
def at_object_creation(self):
|
||||
"called when the object is first created"
|
||||
self.cmdset.add("mycmdset.MyCmdSet", permanent=True)
|
||||
```
|
||||
|
||||
All new objects of this typeclass will now start with this cmdset and it will survive a `@reload`.
|
||||
|
||||
*Note:* An important caveat with this is that `at_object_creation` is only called *once*, when the
|
||||
object is first created. This means that if you already have existing objects in your databases
|
||||
using that typeclass, they will not have been initiated the same way. There are many ways to update
|
||||
them; since it's a one-time update you can usually just simply loop through them. As superuser, try
|
||||
the following:
|
||||
|
||||
@py from typeclasses.objects import MyObject; [o.cmdset.add("mycmdset.MyCmdSet") for o in
|
||||
MyObject.objects.all()]
|
||||
|
||||
This goes through all objects in your database having the right typeclass, adding the new cmdset to
|
||||
each. The good news is that you only have to do this if you want to post-add *cmdsets*. If you just
|
||||
want to add a new *command*, you can simply add that command to the cmdset's `at_cmdset_creation`
|
||||
and `@reload` to make the Command immediately available.
|
||||
|
||||
## Change where Evennia looks for command sets
|
||||
|
||||
Evennia uses settings variables to know where to look for its default command sets. These are
|
||||
normally not changed unless you want to re-organize your game folder in some way. For example, the
|
||||
default character cmdset defaults to being defined as
|
||||
|
||||
CMDSET_CHARACTER="commands.default_cmdset.CharacterCmdSet"
|
||||
|
||||
|
||||
[prev lesson](Python-classes-and-objects) | [next lesson]()
|
||||
[prev lesson](Python-classes-and-objects) | [next lesson](More-on-Commands)
|
||||
|
|
|
|||
303
docs/source/Howto/Starting/Part1/Learning-Typeclasses.md
Normal file
303
docs/source/Howto/Starting/Part1/Learning-Typeclasses.md
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
# Persistent objects and typeclasses
|
||||
|
||||
[prev lesson](Python-classes-and-objects) | [next lesson](Adding-Commands)
|
||||
|
||||
In the last lesson we created the dragons Fluffy, Cuddly and Smaug and made the fly and breathe fire. We
|
||||
learned a bit about _classes_ in the process. But 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/mobile.py` so far:
|
||||
|
||||
|
||||
```python
|
||||
|
||||
class Mobile:
|
||||
"""
|
||||
This is a base class for Mobiles.
|
||||
"""
|
||||
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
|
||||
def move_around(self):
|
||||
print(f"{self.key} is moving!")
|
||||
|
||||
|
||||
class Dragon(Mobile):
|
||||
"""
|
||||
This is a dragon-specific mobile.
|
||||
"""
|
||||
|
||||
def move_around(self):
|
||||
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
|
||||
|
||||
Now we should know enough to understand what is happening in `mygame/typeclasses/objects.py`.
|
||||
Open it again:
|
||||
|
||||
```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](api:evennia.objects.objects#DefaultObject). The docstring for
|
||||
> the `Object` class can also help.
|
||||
|
||||
One thing that Evennia offers and which you don't get with vanilla Python classes is _persistence_. As you've
|
||||
found, Fluffy, Cuddly and Smaug are gone once we reload the server. Let's see if we can fix this.
|
||||
|
||||
Go back to `mygame/typeclasses/mobile.py`. Change it as follows:
|
||||
|
||||
```python
|
||||
|
||||
from typeclasses.objects import Object
|
||||
|
||||
class Mobile(Object):
|
||||
"""
|
||||
This is a base class for Mobiles.
|
||||
"""
|
||||
def move_around(self):
|
||||
print(f"{self.key} is moving!")
|
||||
|
||||
|
||||
class Dragon(Mobile):
|
||||
"""
|
||||
This is a dragon-specific mobile.
|
||||
"""
|
||||
|
||||
def 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 `Mobile` inherit from Evennia's `Object` (which in turn
|
||||
inherits from Evennia's `DefaultObject`, as we saw). By extension, this means that `Dragon` also inherits
|
||||
from `DefaultObject`, just from further away!
|
||||
|
||||
### Creating by calling the class (less common way)
|
||||
|
||||
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.mymobile 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 is to create a new entry in the database for Smaug. We gave the object
|
||||
its name (key) and set its location to our current location (remember that `here` is just something available
|
||||
in the `py` command, you can't use it elsewhere).
|
||||
|
||||
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.mymobile.Mobile', key="Fluffy", location=here)
|
||||
> look
|
||||
|
||||
Boom, Fluffy should now be in the room with you, a little less scary than Smaug. You specify the
|
||||
python-path to the code you want and then set the key and location. Evennia sets things up and saves for you.
|
||||
|
||||
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.mymobile.Mobile
|
||||
|
||||
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.mymobile.Mobile", key="Cuddly", location=here)
|
||||
|
||||
That's pretty much all there is to the mighty `create` command.
|
||||
|
||||
... And speaking of Commands, we should try to add one of our own next.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Adding Object Typeclass Tutorial
|
||||
|
||||
Evennia comes with a few very basic classes of in-game entities:
|
||||
|
||||
DefaultObject
|
||||
|
|
||||
DefaultCharacter
|
||||
DefaultRoom
|
||||
DefaultExit
|
||||
DefaultChannel
|
||||
|
||||
When you create a new Evennia game (with for example `evennia --init mygame`) Evennia will
|
||||
automatically create empty child classes `Object`, `Character`, `Room` and `Exit` respectively. They
|
||||
are found `mygame/typeclasses/objects.py`, `mygame/typeclasses/rooms.py` etc.
|
||||
|
||||
> Technically these are all [Typeclassed](../../../Component/Typeclasses), which can be ignored for now. In
|
||||
> `mygame/typeclasses` are also base typeclasses for out-of-character things, notably
|
||||
> [Channels](../../../Component/Communications), [Accounts](../../../Component/Accounts) and [Scripts](../../../Component/Scripts). We don't cover those in
|
||||
> this tutorial.
|
||||
|
||||
For your own game you will most likely want to expand on these very simple beginnings. It's normal
|
||||
to want your Characters to have various attributes, for example. Maybe Rooms should hold extra
|
||||
information or even *all* Objects in your game should have properties not included in basic Evennia.
|
||||
|
||||
## Change Default Rooms, Exits, Character Typeclass
|
||||
|
||||
This is the simplest case.
|
||||
|
||||
The default build commands of a new Evennia game is set up to use the `Room`, `Exit` and `Character`
|
||||
classes found in the same-named modules under `mygame/typeclasses/`. By default these are empty and
|
||||
just implements the default parents from the Evennia library (`DefaultRoom`etc). Just add the
|
||||
changes you want to these classes and run `@reload` to add your new functionality.
|
||||
|
||||
## Create a new type of object
|
||||
|
||||
Say you want to create a new "Heavy" object-type that characters should not have the ability to pick
|
||||
up.
|
||||
|
||||
1. Edit `mygame/typeclasses/objects.py` (you could also create a new module there, named something
|
||||
like `heavy.py`, that's up to how you want to organize things).
|
||||
1. Create a new class inheriting at any distance from `DefaultObject`. It could look something like
|
||||
this:
|
||||
```python
|
||||
# end of file mygame/typeclasses/objects.py
|
||||
from evennia import DefaultObject
|
||||
|
||||
class Heavy(DefaultObject):
|
||||
"Heavy object"
|
||||
def at_object_creation(self):
|
||||
"Called whenever a new object is created"
|
||||
# lock the object down by default
|
||||
self.locks.add("get:false()")
|
||||
# the default "get" command looks for this Attribute in order
|
||||
# to return a customized error message (we just happen to know
|
||||
# this, you'd have to look at the code of the 'get' command to
|
||||
# find out).
|
||||
self.db.get_err_msg = "This is too heavy to pick up."
|
||||
```
|
||||
1. Once you are done, log into the game with a build-capable account and do `@create/drop
|
||||
rock:objects.Heavy` to drop a new heavy "rock" object in your location. Next try to pick it up
|
||||
(`@quell` yourself first if you are a superuser). If you get errors, look at your log files where
|
||||
you will find the traceback. The most common error is that you have some sort of syntax error in
|
||||
your class.
|
||||
|
||||
Note that the [Locks](../../../Component/Locks) and [Attribute](../../../Component/Attributes) which are set in the typeclass could just
|
||||
as well have been set using commands in-game, so this is a *very* simple example.
|
||||
|
||||
## Storing data on initialization
|
||||
|
||||
The `at_object_creation` is only called once, when the object is first created. This makes it ideal
|
||||
for database-bound things like [Attributes](../../../Component/Attributes). But sometimes you want to create temporary
|
||||
properties (things that are not to be stored in the database but still always exist every time the
|
||||
object is created). Such properties can be initialized in the `at_init` method on the object.
|
||||
`at_init` is called every time the object is loaded into memory.
|
||||
|
||||
> Note: It's usually pointless and wasteful to assign database data in `at_init`, since this will
|
||||
> hit the database with the same value over and over. Put those in `at_object_creation` instead.
|
||||
|
||||
You are wise to use `ndb` (non-database Attributes) to store these non-persistent properties, since
|
||||
ndb-properties are protected against being cached out in various ways and also allows you to list
|
||||
them using various in-game tools:
|
||||
|
||||
```python
|
||||
def at_init(self):
|
||||
self.ndb.counter = 0
|
||||
self.ndb.mylist = []
|
||||
```
|
||||
|
||||
> Note: As mentioned in the [Typeclasses](../../../Component/Typeclasses) documentation, `at_init` replaces the use of
|
||||
> the standard `__init__` method of typeclasses due to how the latter may be called in situations
|
||||
> other than you'd expect. So use `at_init` where you would normally use `__init__`.
|
||||
|
||||
|
||||
## Updating existing objects
|
||||
|
||||
If you already have some `Heavy` objects created and you add a new `Attribute` in
|
||||
`at_object_creation`, you will find that those existing objects will not have this Attribute. This
|
||||
is not so strange, since `at_object_creation` is only called once, it will not be called again just
|
||||
because you update it. You need to update existing objects manually.
|
||||
|
||||
If the number of objects is limited, you can use `@typeclass/force/reload objectname` to force a
|
||||
re-load of the `at_object_creation` method (only) on the object. This case is common enough that
|
||||
there is an alias `@update objectname` you can use to get the same effect. If there are multiple
|
||||
objects you can use `@py` to loop over the objects you need:
|
||||
|
||||
```
|
||||
@py from typeclasses.objects import Heavy; [obj.at_object_creation() for obj in Heavy.objects.all()]
|
||||
|
||||
```
|
||||
|
||||
[prev lesson](Python-classes-and-objects) | [next lesson](Adding-Commands)
|
||||
502
docs/source/Howto/Starting/Part1/More-on-Commands.md
Normal file
502
docs/source/Howto/Starting/Part1/More-on-Commands.md
Normal file
|
|
@ -0,0 +1,502 @@
|
|||
# More about Commands
|
||||
|
||||
[prev lesson](Adding-Commands) | [next lesson](Learning-Typeclasses)
|
||||
|
||||
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](Building-Quickstart)
|
||||
you've seen an example of this with the "Red Button" object. The [Tutorial world](Tutorial-World-Introduction)
|
||||
also has many examples of objects with commands on them.
|
||||
|
||||
To show how this could work, let's put our 'hit' Command on our simple `sword` object from the previous section.
|
||||
|
||||
> self.search("sword").cmdset.add("commands.mycommands.MyCmdSet", permanent=True)
|
||||
|
||||
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 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.
|
||||
|
||||
Let's explore 'ourselves' and other 'things' in the game next.
|
||||
|
||||
[prev lesson](Adding-Commands) | [next lesson](Learning-Typeclasses)
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
# Python Classes and Evennia Typeclasses
|
||||
# Python Classes and objects
|
||||
|
||||
[prev lesson](Gamedir-Overview) | [next lesson](Adding-Commands)
|
||||
[prev lesson](Gamedir-Overview) | [next lesson](Learning-Typeclasses)
|
||||
|
||||
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
|
||||
## 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
|
||||
|
|
@ -153,7 +153,9 @@ Next we have a `class` named `Object`, which _inherits_ from `DefaultObject`. Th
|
|||
actually do anything on its own, its only code (except the docstring) is `pass` which means,
|
||||
well, to pass and don't do anything.
|
||||
|
||||
To understand what we are looking at, we need to explain what a 'class', an 'object' and an 'instance' is.
|
||||
We will get back to this module in the [next lesson](Learning-Typeclasses). 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
|
||||
|
|
@ -185,6 +187,12 @@ at least one argument (almost always written as `self` although you could in pri
|
|||
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 tin 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
|
||||
`Mobile` is a class, then an instance is Fluffy, the individual red dragon. You instantiate
|
||||
by _calling_ the class, much like you would a function:
|
||||
|
|
@ -395,152 +403,14 @@ 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
|
||||
|
||||
## Our first persistent object
|
||||
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.
|
||||
|
||||
Now we should know enough to understand what is happening in `mygame/typeclasses/objects.py`.
|
||||
Open it again:
|
||||
|
||||
```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](api:evennia.objects.objects#DefaultObject). The docstring for
|
||||
> the `Object` class can also help.
|
||||
|
||||
One thing that Evennia offers and which you don't get with vanilla Python classes is _persistence_. As you've
|
||||
found, Fluffy, Cuddly and Smaug are gone once we reload the server. Let's see if we can fix this.
|
||||
|
||||
Go back to `mygame/typeclasses/mobile.py`. Change it as follows:
|
||||
|
||||
```python
|
||||
|
||||
from typeclasses.objects import Object
|
||||
|
||||
class Mobile(Object):
|
||||
"""
|
||||
This is a base class for Mobiles.
|
||||
"""
|
||||
def move_around(self):
|
||||
print(f"{self.key} is moving!")
|
||||
But so far our dragons are gone as soon as we `restart` the server or `quit()` the Python interpreter. In the
|
||||
next lesson we'll get up close and personal with Smaug.
|
||||
|
||||
|
||||
class Dragon(Mobile):
|
||||
"""
|
||||
This is a dragon-specific mobile.
|
||||
"""
|
||||
|
||||
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 `Mobile` inherit from Evennia's `Object` (which in turn
|
||||
inherits from Evennia's `DefaultObject`, as we saw). By extension, this means that `Dragon` also inherits
|
||||
from `DefaultObject`, just from further away!
|
||||
|
||||
### Creating by calling the class (less common way)
|
||||
|
||||
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.mymobile 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 is to create a new entry in the database for Smaug. We gave the object
|
||||
its name (key) and set its location to our current location (remember that `here` is just something available
|
||||
in the `py` command, you can't use it elsewhere).
|
||||
|
||||
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.mymobile.Mobile', key="Fluffy", location=here)
|
||||
> look
|
||||
|
||||
Boom, Fluffy should now be in the room with you, a little less scary than Smaug. You specify the
|
||||
python-path to the code you want and then set the key and location. Evennia sets things up and saves for you.
|
||||
|
||||
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.mymobile.Mobile
|
||||
|
||||
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.mymobile.Mobile", key="Cuddly", location=here)
|
||||
|
||||
That's pretty much all there is to the mighty `create` command.
|
||||
|
||||
... And speaking of Commands, we should try to add one of our own next.
|
||||
|
||||
|
||||
[prev lesson](Gamedir-Overview) | [next lesson](Adding-Commands)
|
||||
[prev lesson](Gamedir-Overview) | [next lesson](Learning-Typeclasses)
|
||||
|
|
|
|||
|
|
@ -26,10 +26,11 @@ own first little game in Evennia. Let's get started!
|
|||
1. [The Tutorial World](Part1/Tutorial-World-Introduction)
|
||||
1. [Python basics](Part1/Python-basic-introduction)
|
||||
1. [Game dir overview](Part1/Gamedir-Overview)
|
||||
1. [Python classes](Python-basic-tutorial-part-two)
|
||||
1. [Running Python in- and outside the game](../../Coding/Execute-Python-Code)
|
||||
1. [Understanding errors](Understanding-Errors)
|
||||
1. [Searching for things](Tutorial-Searching-For-Objects)
|
||||
1. [Python classes and objects](Part1/Python-classes-and-objects)
|
||||
1. [Persistent objects](Part1/Learning-Typeclasses)
|
||||
1. [Making our first own commands](Part1/Adding-Commands)
|
||||
1. [Parsing and replacing default Commands](Part1/More-on-Commands)
|
||||
1. [Searching and creating things](Tutorial-Searching-For-Objects)
|
||||
1. [A walkthrough of the API](Walkthrough-of-API)
|
||||
|
||||
In this first part we'll focus on what we get out of the box in Evennia - we'll get used to the tools,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue