Made a lot of progress on the search tutorial
This commit is contained in:
parent
3f1a3612e4
commit
c54625b305
15 changed files with 786 additions and 636 deletions
|
|
@ -52,7 +52,7 @@ using such a checker can be a good start to weed out the simple problems.
|
||||||
|
|
||||||
### Plan before you code
|
### Plan before you code
|
||||||
|
|
||||||
Before you start coding away at your dream game, take a look at our [Game Planning](../Howto/Starting/Game-Planning)
|
Before you start coding away at your dream game, take a look at our [Game Planning](../Howto/Starting/Part2/Game-Planning)
|
||||||
page. It might hopefully help you avoid some common pitfalls and time sinks.
|
page. It might hopefully help you avoid some common pitfalls and time sinks.
|
||||||
|
|
||||||
### Code in your game folder, not in the evennia/ repository
|
### Code in your game folder, not in the evennia/ repository
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,7 @@ chat](http://webchat.freenode.net/?channels=evennia&uio=MT1mYWxzZSY5PXRydWUmMTE9
|
||||||
on IRC. This allows you to chat directly with other developers new and old as well as with the devs
|
on IRC. This allows you to chat directly with other developers new and old as well as with the devs
|
||||||
of Evennia itself. This chat is logged (you can find links on http://www.evennia.com) and can also
|
of Evennia itself. This chat is logged (you can find links on http://www.evennia.com) and can also
|
||||||
be searched from the same place for discussion topics you are interested in.
|
be searched from the same place for discussion topics you are interested in.
|
||||||
2. Read the [Game Planning](Howto/Starting/Game-Planning) wiki page. It gives some ideas for your work flow and the
|
2. Read the [Game Planning](Howto/Starting/Part2/Game-Planning) wiki page. It gives some ideas for your work flow and the
|
||||||
state of mind you should aim for - including cutting down the scope of your game for its first
|
state of mind you should aim for - including cutting down the scope of your game for its first
|
||||||
release.
|
release.
|
||||||
3. Do the [Tutorial for basic MUSH-like game](Howto/Starting/Tutorial-for-basic-MUSH-like-game) carefully from
|
3. Do the [Tutorial for basic MUSH-like game](Howto/Starting/Tutorial-for-basic-MUSH-like-game) carefully from
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ in mind for your own game, this will give you a good start.
|
||||||
### Part 2: What we want
|
### Part 2: What we want
|
||||||
|
|
||||||
1. [Introduction & Overview](Starting/Starting-Part2)
|
1. [Introduction & Overview](Starting/Starting-Part2)
|
||||||
1. [On planning a game](Starting/Game-Planning)
|
1. [On planning a game](Starting/Part2/Game-Planning)
|
||||||
1. [Multisession modes](Multi-session-modes)
|
1. [Multisession modes](Multi-session-modes)
|
||||||
1. [Layout of our tutorial game](Game-Tutorial-Planning)
|
1. [Layout of our tutorial game](Game-Tutorial-Planning)
|
||||||
1. [Making use of contribs](Using-Contribs)
|
1. [Making use of contribs](Using-Contribs)
|
||||||
|
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
## Evennia API overview
|
|
||||||
|
|
||||||
If you cloned the GIT repo following the instructions, you will have a folder named `evennia`. The
|
|
||||||
top level of it contains Python package specific stuff such as a readme file, `setup.py` etc. It
|
|
||||||
also has two subfolders`bin/` and `evennia/` (again).
|
|
||||||
|
|
||||||
The `bin/` directory holds OS-specific binaries that will be used when installing Evennia with `pip`
|
|
||||||
as per the [Getting started](../Setup/Getting-Started) instructions. The library itself is in the `evennia`
|
|
||||||
subfolder. From your code you will access this subfolder simply by `import evennia`.
|
|
||||||
|
|
||||||
- evennia
|
|
||||||
- [`__init__.py`](Evennia-API) - The "flat API" of Evennia resides here.
|
|
||||||
- [`commands/`](Commands) - The command parser and handler.
|
|
||||||
- `default/` - The [default commands](../../Component/Default-Command-Help) and cmdsets.
|
|
||||||
- [`comms/`](Communications) - 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/`](Help-System) - Handles the storage and creation of help entries.
|
|
||||||
- `locale/` - Language files ([i18n](../../Concept/Internationalization)).
|
|
||||||
- [`locks/`](Locks) - Lock system for restricting access to in-game entities.
|
|
||||||
- [`objects/`](Objects) - In-game entities (all types of items and Characters).
|
|
||||||
- [`prototypes/`](Spawner-and-Prototypes) - Object Prototype/spawning system and OLC menu
|
|
||||||
- [`accounts/`](Accounts) - Out-of-game Session-controlled entities (accounts, bots etc)
|
|
||||||
- [`scripts/`](Scripts) - Out-of-game entities equivalence to Objects, also with timer support.
|
|
||||||
- [`server/`](Portal-And-Server) - Core server code and Session handling.
|
|
||||||
- `portal/` - Portal proxy and connection protocols.
|
|
||||||
- [`settings_default.py`](Server-Conf#Settings-file) - Root settings of Evennia. Copy settings
|
|
||||||
from here to `mygame/server/settings.py` file.
|
|
||||||
- [`typeclasses/`](Typeclasses) - Abstract classes for the typeclass storage and database system.
|
|
||||||
- [`utils/`](Coding-Utils) - Various miscellaneous useful coding resources.
|
|
||||||
- [`web/`](Web-Features) - Web resources and webserver. Partly copied into game directory on
|
|
||||||
initialization.
|
|
||||||
|
|
||||||
All directories contain files ending in `.py`. These are Python *modules* and are the basic units of
|
|
||||||
Python code. The roots of directories also have (usually empty) files named `__init__.py`. These are
|
|
||||||
required by Python so as to be able to find and import modules in other directories. When you have
|
|
||||||
run Evennia at least once you will find that there will also be `.pyc` files appearing, these are
|
|
||||||
pre-compiled binary versions of the `.py` files to speed up execution.
|
|
||||||
|
|
||||||
The root of the `evennia` folder has an `__init__.py` file containing the "[flat API](../../Evennia-API)".
|
|
||||||
This holds shortcuts to various subfolders in the evennia library. It is provided to make it easier
|
|
||||||
to find things; it allows you to just import `evennia` and access things from that rather than
|
|
||||||
having to import from their actual locations inside the source tree.
|
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Our own commands
|
# Our own commands
|
||||||
|
|
||||||
[prev lesson](Python-classes-and-objects) | [next lesson](More-on-Commands)
|
[prev lesson](Searching-Things) | [next lesson]()
|
||||||
|
|
||||||
In this lesson we'll learn how to create our own Evennia _Commands_. If you are new to Python you'll
|
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.
|
also learn some more basics about how to manipulate strings and get information out of Evennia.
|
||||||
|
|
@ -393,5 +393,4 @@ We also upset a dragon.
|
||||||
In the next lesson we'll learn how to hit Smaug with different weapons. We'll also
|
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.
|
get into how we replace and extend Evennia's default Commands.
|
||||||
|
|
||||||
|
[prev lesson](Searching-Things) | [next lesson]()
|
||||||
[prev lesson](Python-classes-and-objects) | [next lesson](More-on-Commands)
|
|
||||||
|
|
|
||||||
53
docs/source/Howto/Starting/Part1/Creating-Things.md
Normal file
53
docs/source/Howto/Starting/Part1/Creating-Things.md
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
# Creating things
|
||||||
|
|
||||||
|
[prev lesson](Learning-Typeclasses) | [next lesson](Searching-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).
|
||||||
|
- 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_
|
||||||
|
|
||||||
|
|
||||||
|
[prev lesson](Learning-Typeclasses) | [next lesson](Searching-Things)
|
||||||
|
|
||||||
|
|
@ -1,562 +0,0 @@
|
||||||
# Overview of the Evennia API
|
|
||||||
|
|
||||||
In the last few lessons we have explored the gamedir, learned about typeclasses and commands. In the process
|
|
||||||
we have used several resources from the Evennia library. Some examples:
|
|
||||||
|
|
||||||
- `evennia.DefaultObject`, `evennia.DefaultCharacter` and (inherited) methods on these classes like `.msg`
|
|
||||||
and `at_object_create` but also `.cmdset.add` for adding new cmdsets.
|
|
||||||
- `evennia.search_object` for finding lists of objects anywhere.
|
|
||||||
- `evennia.create_object` for creating objects in code instead of using the in-game `create` command.
|
|
||||||
- `evennia.Command` with methods like `func` and `parse` to implement new commands
|
|
||||||
- `evennia.CmdSet` for storing commands
|
|
||||||
- `evennia.default_cmds` holding references to all default Command classes like `look`, `dig` and so on.
|
|
||||||
|
|
||||||
Evennia has a lot of resources to help you make your game. We have just given a selection of them for you to try
|
|
||||||
out so far (and we'll show off many more in the lessons to come). Now we'll teach you how to find them
|
|
||||||
for yourself.
|
|
||||||
|
|
||||||
## Exploring the API
|
|
||||||
|
|
||||||
The Evennia _API_
|
|
||||||
([Application Programming Interface](https://en.wikipedia.org/wiki/Application_programming_interface)) is what
|
|
||||||
you use to access things inside the `evennia` package.
|
|
||||||
|
|
||||||
Open the [API frontpage](../../../Evennia-API). 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.
|
|
||||||
|
|
||||||
### Browsing the code
|
|
||||||
|
|
||||||
You can 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.
|
|
||||||
|
|
||||||
You can also 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/Extended-Installation) if you want to do this. The short of is to install `git` and run
|
|
||||||
|
|
||||||
git clone https://github.com/evennia/evennia.git
|
|
||||||
|
|
||||||
In the terminal/console you can search for anything using `git` (make sure you are inside the repo):
|
|
||||||
|
|
||||||
git grep -n "class DefaultObject"
|
|
||||||
|
|
||||||
will quickly tell you which file the DefaultObject class is defined and on which line.
|
|
||||||
|
|
||||||
If you read the code on `github` or cloned the repo yourself, you will find this being the outermost folder
|
|
||||||
structure:
|
|
||||||
|
|
||||||
evennia/
|
|
||||||
bin/
|
|
||||||
CHANGELOG.md
|
|
||||||
...
|
|
||||||
...
|
|
||||||
docs/
|
|
||||||
evennia/
|
|
||||||
|
|
||||||
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 outermost level is part of the Evennia package distribution and
|
|
||||||
installation. It's not something we'll bother with for this tutorial.
|
|
||||||
|
|
||||||
> The `evennia/docs/` folder contains, well, this documentation. See [contributing to the docs](../../../Contributing-Docs) if you
|
|
||||||
want to learn more about how this works.
|
|
||||||
|
|
||||||
## Overview of the `evennia` root package
|
|
||||||
|
|
||||||
```
|
|
||||||
evennia/
|
|
||||||
__init__.py
|
|
||||||
settings_default.py
|
|
||||||
accounts/
|
|
||||||
commands/
|
|
||||||
comms/
|
|
||||||
game_template/
|
|
||||||
help/
|
|
||||||
locale/
|
|
||||||
locks/
|
|
||||||
objects/
|
|
||||||
prototypes/
|
|
||||||
scripts/
|
|
||||||
server/
|
|
||||||
typeclasses/
|
|
||||||
utils/
|
|
||||||
web/
|
|
||||||
```
|
|
||||||
|
|
||||||
```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 in the same
|
|
||||||
location.
|
|
||||||
|
|
||||||
```
|
|
||||||
While all the actual Evennia code is found in the various folders, the `__init__.py` contains "shortcuts"
|
|
||||||
to useful things you will often need. This allows you to do things like `from evennia import DefaultObject`
|
|
||||||
even though the `DefaultObject` is not actually defined there. Let's see how that works:
|
|
||||||
|
|
||||||
[Look at Line 189](evennia/__init__.py#L189) of `evennia/__init__.py` and you'll find this line:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# ...
|
|
||||||
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.
|
|
||||||
```
|
|
||||||
|
|
||||||
Since `DefaultObject` is imported into `__init__.py`, it means we can do
|
|
||||||
`from evennia import DefaultObject` even though the code for it is not actually here.
|
|
||||||
|
|
||||||
So if we want to find the code for `DefaultObject` we need to look in
|
|
||||||
`evennia/objects/objects.py`. Here's how to look it up in these docs:
|
|
||||||
|
|
||||||
1. Open the [API frontpage](../../../Evennia-API)
|
|
||||||
2. Locate the link to [evennia.objects](api:evennia.objects) and click on it.
|
|
||||||
3. Click through to [evennia.objects.objects](api:evennia.objects.objects).
|
|
||||||
4. You are now in the python module. Scroll down (or search in your web browser) to find the `DefaultObject` class.
|
|
||||||
5. You can now read what this does and what methods are on it. If you want to see the full source, click the
|
|
||||||
\[[source](src:evennia.objects.objects#DefaultObject)\] link.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Tutorial Searching For Objects
|
|
||||||
|
|
||||||
You will often want to operate on a specific object in the database. For example when a player
|
|
||||||
attacks a named target you'll need to find that target so it can be attacked. Or when a rain storm
|
|
||||||
draws in you need to find all outdoor-rooms so you can show it raining in them. This tutorial
|
|
||||||
explains Evennia's tools for searching.
|
|
||||||
|
|
||||||
## Things to search for
|
|
||||||
|
|
||||||
The first thing to consider is the base type of the thing you are searching for. Evennia organizes
|
|
||||||
its database into a few main tables: [Objects](../../../Component/Objects), [Accounts](../../../Component/Accounts), [Scripts](../../../Component/Scripts),
|
|
||||||
[Channels](../../../Component/Communications#channels), [Messages](Communication#Msg) and [Help Entries](../../../Component/Help-System).
|
|
||||||
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?
|
|
||||||
|
|
||||||
- The `key` is the name of the entity. While you can get this from `obj.key` the *database field*
|
|
||||||
is actually named `obj.db_key` - this is useful to know only when you do [direct database
|
|
||||||
queries](Tutorial-Searching-For-Objects#queries-in-django). The one exception is `Accounts`, where
|
|
||||||
the database field for `.key` is instead named `username` (this is a Django requirement). When you
|
|
||||||
don't specify search-type, you'll usually search based on key. *Aliases* are extra names given to
|
|
||||||
Objects using something like `@alias` or `obj.aliases.add('name')`. The main search functions (see
|
|
||||||
below) will automatically search for aliases whenever you search by-key.
|
|
||||||
- [Tags](../../../Component/Tags) are the main way to group and identify objects in Evennia. Tags can most often be
|
|
||||||
used (sometimes together with keys) to uniquely identify an object. For example, even though you
|
|
||||||
have two locations with the same name, you can separate them by their tagging (this is how Evennia
|
|
||||||
implements 'zones' seen in other systems). Tags can also have categories, to further organize your
|
|
||||||
data for quick lookups.
|
|
||||||
- An object's [Attributes](../../../Component/Attributes) can also used to find an object. This can be very useful but
|
|
||||||
since Attributes can store almost any data they are far less optimized to search for than Tags or
|
|
||||||
keys.
|
|
||||||
- The object's [Typeclass](../../../Component/Typeclasses) indicate the sub-type of entity. A Character, Flower or
|
|
||||||
Sword are all types of Objects. A Bot is a kind of Account. The database field is called
|
|
||||||
`typeclass_path` and holds the full Python-path to the class. You can usually specify the
|
|
||||||
`typeclass` as an argument to Evennia's search functions as well as use the class directly to limit
|
|
||||||
queries.
|
|
||||||
- The `location` is only relevant for [Objects](../../../Component/Objects) but is a very common way to weed down the
|
|
||||||
number of candidates before starting to search. The reason is that most in-game commands tend to
|
|
||||||
operate on things nearby (in the same room) so the choices can be limited from the start.
|
|
||||||
- The database id or the '#dbref' is unique (and never re-used) within each database table. So while
|
|
||||||
there is one and only one Object with dbref `#42` there could also be an Account or Script with the
|
|
||||||
dbref `#42` at the same time. In almost all search methods you can replace the "key" search
|
|
||||||
criterion with `"#dbref"` to search for that id. This can occasionally be practical and may be what
|
|
||||||
you are used to from other code bases. But it is considered *bad practice* in Evennia to rely on
|
|
||||||
hard-coded #dbrefs to do your searches. It makes your code tied to the exact layout of the database.
|
|
||||||
It's also not very maintainable to have to remember abstract numbers. Passing the actual objects
|
|
||||||
around and searching by Tags and/or keys will usually get you what you need.
|
|
||||||
|
|
||||||
|
|
||||||
## Getting objects inside another
|
|
||||||
|
|
||||||
All in-game [Objects](../../../Component/Objects) have a `.contents` property that returns all objects 'inside' them
|
|
||||||
(that is, all objects which has its `.location` property set to that object. This is a simple way to
|
|
||||||
get everything in a room and is also faster since this lookup is cached and won't hit the database.
|
|
||||||
|
|
||||||
- `roomobj.contents` returns a list of all objects inside `roomobj`.
|
|
||||||
- `obj.contents` same as for a room, except this usually represents the object's inventory
|
|
||||||
- `obj.location.contents` gets everything in `obj`'s location (including `obj` itself).
|
|
||||||
- `roomobj.exits` returns all exits starting from `roomobj` (Exits are here defined as Objects with
|
|
||||||
their `destination` field set).
|
|
||||||
- `obj.location.contents_get(exclude=obj)` - this helper method returns all objects in `obj`'s
|
|
||||||
location except `obj`.
|
|
||||||
|
|
||||||
## Searching using `Object.search`
|
|
||||||
|
|
||||||
Say you have a [command](../../../Component/Commands), and you want it to do something to a target. You might be
|
|
||||||
wondering how you retrieve that target in code, and that's where Evennia's search utilities come in.
|
|
||||||
In the most common case, you'll often use the `search` method of the `Object` or `Account`
|
|
||||||
typeclasses. In a command, the `.caller` property will refer back to the object using the command
|
|
||||||
(usually a `Character`, which is a type of `Object`) while `.args` will contain Command's arguments:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# e.g. in file mygame/commands/command.py
|
|
||||||
|
|
||||||
from evennia import default_cmds
|
|
||||||
|
|
||||||
class CmdPoke(default_cmds.MuxCommand):
|
|
||||||
"""
|
|
||||||
Pokes someone.
|
|
||||||
|
|
||||||
Usage: poke <target>
|
|
||||||
"""
|
|
||||||
key = "poke"
|
|
||||||
|
|
||||||
def func(self):
|
|
||||||
"""Executes poke command"""
|
|
||||||
target = self.caller.search(self.args)
|
|
||||||
if not target:
|
|
||||||
# we didn't find anyone, but search has already let the
|
|
||||||
# caller know. We'll just return, since we're done
|
|
||||||
return
|
|
||||||
# we found a target! we'll do stuff to them.
|
|
||||||
target.msg("You have been poked by %s." % self.caller)
|
|
||||||
self.caller.msg("You have poked %s." % target)
|
|
||||||
```
|
|
||||||
By default, the search method of a Character will attempt to find a unique object match for the
|
|
||||||
string sent to it (`self.args`, in this case, which is the arguments passed to the command by the
|
|
||||||
player) in the surroundings of the Character - the room or their inventory. If there is no match
|
|
||||||
found, the return value (which is assigned to `target`) will be `None`, and an appropriate failure
|
|
||||||
message will be sent to the Character. If there's not a unique match, `None` will again be returned,
|
|
||||||
and a different error message will be sent asking them to disambiguate the multi-match. By default,
|
|
||||||
the user can then pick out a specific match using with a number and dash preceding the name of the
|
|
||||||
object: `character.search("2-pink unicorn")` will try to find the second pink unicorn in the room.
|
|
||||||
|
|
||||||
The search method has many [arguments](github:evennia.objects.objects#defaultcharactersearch) that
|
|
||||||
allow you to refine the search, such as by designating the location to search in or only matching
|
|
||||||
specific typeclasses.
|
|
||||||
|
|
||||||
## Searching using `utils.search`
|
|
||||||
|
|
||||||
Sometimes you will want to find something that isn't tied to the search methods of a character or
|
|
||||||
account. In these cases, Evennia provides a [utility module with a number of search
|
|
||||||
functions](github:evennia.utils.search). For example, suppose you want a command that will find and
|
|
||||||
display all the rooms that are tagged as a 'hangout', for people to gather by. Here's a simple
|
|
||||||
Command to do this:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# e.g. in file mygame/commands/command.py
|
|
||||||
|
|
||||||
from evennia import default_cmds
|
|
||||||
from evennia.utils.search import search_tag
|
|
||||||
|
|
||||||
class CmdListHangouts(default_cmds.MuxCommand):
|
|
||||||
"""Lists hangouts"""
|
|
||||||
key = "hangouts"
|
|
||||||
|
|
||||||
def func(self):
|
|
||||||
"""Executes 'hangouts' command"""
|
|
||||||
hangouts = search_tag(key="hangout",
|
|
||||||
category="location tags")
|
|
||||||
self.caller.msg("Hangouts available: {}".format(
|
|
||||||
", ".join(str(ob) for ob in hangouts)))
|
|
||||||
```
|
|
||||||
|
|
||||||
This uses the `search_tag` function to find all objects previously tagged with [Tags](../../../Component/Tags)
|
|
||||||
"hangout" and with category "location tags".
|
|
||||||
|
|
||||||
Other important search methods in `utils.search` are
|
|
||||||
|
|
||||||
- `search_object`
|
|
||||||
- `search_account`
|
|
||||||
- `search_scripts`
|
|
||||||
- `search_channel`
|
|
||||||
- `search_message`
|
|
||||||
- `search_help`
|
|
||||||
- `search_tag` - find Objects with a given Tag.
|
|
||||||
- `search_account_tag` - find Accounts with a given Tag.
|
|
||||||
- `search_script_tag` - find Scripts with a given Tag.
|
|
||||||
- `search_channel_tag` - find Channels with a given Tag.
|
|
||||||
- `search_object_attribute` - find Objects with a given Attribute.
|
|
||||||
- `search_account_attribute` - find Accounts with a given Attribute.
|
|
||||||
- `search_attribute_object` - this returns the actual Attribute, not the object it sits on.
|
|
||||||
|
|
||||||
> Note: All search functions return a Django `queryset` which is technically a list-like
|
|
||||||
representation of the database-query it's about to do. Only when you convert it to a real list, loop
|
|
||||||
over it or try to slice or access any of its contents will the datbase-lookup happen. This means you
|
|
||||||
could yourself customize the query further if you know what you are doing (see the next section).
|
|
||||||
|
|
||||||
## Queries in Django
|
|
||||||
|
|
||||||
*This is an advanced topic.*
|
|
||||||
|
|
||||||
Evennia's search methods should be sufficient for the vast majority of situations. But eventually
|
|
||||||
you might find yourself trying to figure out how to get searches for unusual circumstances: Maybe
|
|
||||||
you want to find all characters who are *not* in rooms tagged as hangouts *and* have the lycanthrope
|
|
||||||
tag *and* whose names start with a vowel, but *not* with 'Ab', and *only if* they have 3 or more
|
|
||||||
objects in their inventory ... You could in principle use one of the earlier search methods to find
|
|
||||||
all candidates and then loop over them with a lot of if statements in raw Python. But you can do
|
|
||||||
this much more efficiently by querying the database directly.
|
|
||||||
|
|
||||||
Enter [django's querysets](https://docs.djangoproject.com/en/1.11/ref/models/querysets/). A QuerySet
|
|
||||||
is the representation of a database query and can be modified as desired. Only once one tries to
|
|
||||||
retrieve the data of that query is it *evaluated* and does an actual database request. This is
|
|
||||||
useful because it means you can modify a query as much as you want (even pass it around) and only
|
|
||||||
hit the database once you are happy with it.
|
|
||||||
Evennia's search functions are themselves an even higher level wrapper around Django's queries, and
|
|
||||||
many search methods return querysets. That means that you could get the result from a search
|
|
||||||
function and modify the resulting query to your own ends to further tweak what you search for.
|
|
||||||
|
|
||||||
Evaluated querysets can either contain objects such as Character objects, or lists of values derived
|
|
||||||
from the objects. Queries usually use the 'manager' object of a class, which by convention is the
|
|
||||||
`.objects` attribute of a class. For example, a query of Accounts that contain the letter 'a' could
|
|
||||||
be:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from typeclasses.accounts import Account
|
|
||||||
|
|
||||||
queryset = Account.objects.filter(username__contains='a')
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
The `filter` method of a manager takes arguments that allow you to define the query, and you can
|
|
||||||
continue to refine the query by calling additional methods until you evaluate the queryset, causing
|
|
||||||
the query to be executed and return a result. For example, if you have the result above, you could,
|
|
||||||
without causing the queryset to be evaluated yet, get rid of matches that contain the letter 'e by
|
|
||||||
doing this:
|
|
||||||
|
|
||||||
```python
|
|
||||||
queryset = result.exclude(username__contains='e')
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
> You could also have chained `.exclude` directly to the end of the previous line.
|
|
||||||
|
|
||||||
Once you try to access the result, the queryset will be evaluated automatically under the hood:
|
|
||||||
|
|
||||||
```python
|
|
||||||
accounts = list(queryset) # this fills list with matches
|
|
||||||
|
|
||||||
for account in queryset:
|
|
||||||
# do something with account
|
|
||||||
|
|
||||||
accounts = queryset[:4] # get first four matches
|
|
||||||
account = queryset[0] # get first match
|
|
||||||
# etc
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Limiting by typeclass
|
|
||||||
|
|
||||||
Although `Character`s, `Exit`s, `Room`s, and other children of `DefaultObject` all shares the same
|
|
||||||
underlying database table, Evennia provides a shortcut to do more specific queries only for those
|
|
||||||
typeclasses. For example, to find only `Character`s whose names start with 'A', you might do:
|
|
||||||
|
|
||||||
```python
|
|
||||||
Character.objects.filter(db_key__startswith="A")
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
If Character has a subclass `Npc` and you wanted to find only Npc's you'd instead do
|
|
||||||
|
|
||||||
```python
|
|
||||||
Npc.objects.filter(db_key__startswith="A")
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
If you wanted to search both Characters and all its subclasses (like Npc) you use the `*_family`
|
|
||||||
method which is added by Evennia:
|
|
||||||
|
|
||||||
|
|
||||||
```python
|
|
||||||
Character.objects.filter_family(db_key__startswith="A")
|
|
||||||
```
|
|
||||||
|
|
||||||
The higher up in the inheritance hierarchy you go the more objects will be included in these
|
|
||||||
searches. There is one special case, if you really want to include *everything* from a given
|
|
||||||
database table. You do that by searching on the database model itself. These are named `ObjectDB`,
|
|
||||||
`AccountDB`, `ScriptDB` etc.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from evennia import AccountDB
|
|
||||||
|
|
||||||
# all Accounts in the database, regardless of typeclass
|
|
||||||
all = AccountDB.objects.all()
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Multiple conditions
|
|
||||||
|
|
||||||
If you pass more than one keyword argument to a query method, the query becomes an `AND`
|
|
||||||
relationship. For example, if we want to find characters whose names start with "A" *and* are also
|
|
||||||
werewolves (have the `lycanthrope` tag), we might do:
|
|
||||||
|
|
||||||
```python
|
|
||||||
queryset = Character.objects.filter(db_key__startswith="A", db_tags__db_key="lycanthrope")
|
|
||||||
```
|
|
||||||
|
|
||||||
To exclude lycanthropes currently in rooms tagged as hangouts, we might tack on an `.exclude` as
|
|
||||||
before:
|
|
||||||
|
|
||||||
```python
|
|
||||||
queryset = quersyet.exclude(db_location__db_tags__db_key="hangout")
|
|
||||||
```
|
|
||||||
|
|
||||||
Note the syntax of the keywords in building the queryset. For example, `db_location` is the name of
|
|
||||||
the database field sitting on (in this case) the `Character` (Object). Double underscore `__` works
|
|
||||||
like dot-notation in normal Python (it's used since dots are not allowed in keyword names). So the
|
|
||||||
instruction `db_location__db_tags__db_key="hangout"` should be read as such:
|
|
||||||
|
|
||||||
1. "On the `Character` object ... (this comes from us building this queryset using the
|
|
||||||
`Character.objects` manager)
|
|
||||||
2. ... get the value of the `db_location` field ... (this references a Room object, normally)
|
|
||||||
3. ... on that location, get the value of the `db_tags` field ... (this is a many-to-many field that
|
|
||||||
can be treated like an object for this purpose. It references all tags on the location)
|
|
||||||
4. ... through the `db_tag` manager, find all Tags having a field `db_key` set to the value
|
|
||||||
"hangout"."
|
|
||||||
|
|
||||||
This may seem a little complex at first, but this syntax will work the same for all queries. Just
|
|
||||||
remember that all *database-fields* in Evennia are prefaced with `db_`. So even though Evennia is
|
|
||||||
nice enough to alias the `db_key` field so you can normally just do `char.key` to get a character's
|
|
||||||
name, the database field is actually called `db_key` and the real name must be used for the purpose
|
|
||||||
of building a query.
|
|
||||||
|
|
||||||
> Don't confuse database fields with [Attributes](../../../Component/Attributes) 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. You can get attached Attributes
|
|
||||||
manually through the `db_attributes` many-to-many field in the same way as `db_tags` above.
|
|
||||||
|
|
||||||
### Complex queries
|
|
||||||
|
|
||||||
What if you want to have a query with with `OR` conditions or negated requirements (`NOT`)? Enter
|
|
||||||
Django's Complex Query object,
|
|
||||||
[Q](https://docs.djangoproject.com/en/1.11/topics/db/queries/#complex-lookups-with-q-objects). `Q()`
|
|
||||||
objects take a normal django keyword query as its arguments. The special thing is that these Q
|
|
||||||
objects can then be chained together with set operations: `|` for OR, `&` for AND, and preceded with
|
|
||||||
`~` for NOT to build a combined, complex query.
|
|
||||||
|
|
||||||
In our original Lycanthrope example we wanted our werewolves to have names that could start with any
|
|
||||||
vowel except for the specific beginning "ab".
|
|
||||||
|
|
||||||
```python
|
|
||||||
from django.db.models import Q
|
|
||||||
from typeclasses.characters import Character
|
|
||||||
|
|
||||||
query = Q()
|
|
||||||
for letter in ("aeiouy"):
|
|
||||||
query |= Q(db_key__istartswith=letter)
|
|
||||||
query &= ~Q(db_key__istartswith="ab")
|
|
||||||
query = Character.objects.filter(query)
|
|
||||||
|
|
||||||
list_of_lycanthropes = list(query)
|
|
||||||
```
|
|
||||||
|
|
||||||
In the above example, we construct our query our of several Q objects that each represent one part
|
|
||||||
of the query. We iterate over the list of vowels, and add an `OR` condition to the query using `|=`
|
|
||||||
(this is the same idea as using `+=` which may be more familiar). Each `OR` condition checks that
|
|
||||||
the name starts with one of the valid vowels. Afterwards, we add (using `&=`) an `AND` condition
|
|
||||||
that is negated with the `~` symbol. In other words we require that any match should *not* start
|
|
||||||
with the string "ab". Note that we don't actually hit the database until we convert the query to a
|
|
||||||
list at the end (we didn't need to do that either, but could just have kept the query until we
|
|
||||||
needed to do something with the matches).
|
|
||||||
|
|
||||||
### Annotations and `F` objects
|
|
||||||
|
|
||||||
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* retrieve all interesting candidates and run them through a for-loop to get and count
|
|
||||||
their `.content` properties. We'd then just return a list of only those objects with enough
|
|
||||||
contents. It would look something like this (note: don't actually do this!):
|
|
||||||
|
|
||||||
```python
|
|
||||||
# probably not a good idea to do it this way
|
|
||||||
|
|
||||||
from typeclasses.rooms import Room
|
|
||||||
|
|
||||||
queryset = Room.objects.all() # get all Rooms
|
|
||||||
rooms = [room for room in queryset if len(room.contents) >= 5]
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Once the number of rooms in your game increases, this could become quite expensive. Additionally, in
|
|
||||||
some particular contexts, like when using the web features of Evennia, you must have the result as a
|
|
||||||
queryset in order to use it in operations, such as in Django's admin interface when creating list
|
|
||||||
filters.
|
|
||||||
|
|
||||||
Enter [F objects](https://docs.djangoproject.com/en/1.11/ref/models/expressions/#f-expressions) and
|
|
||||||
*annotations*. So-called F expressions allow you to do a query that looks at a value of each object
|
|
||||||
in the database, while annotations allow you to calculate and attach a value to a query. So, let's
|
|
||||||
do the same example as before directly in the database:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from typeclasses.rooms import Room
|
|
||||||
from django.db.models import Count
|
|
||||||
|
|
||||||
room_count = Room.objects.annotate(num_objects=Count('locations_set'))
|
|
||||||
queryset = room_count.filter(num_objects__gte=5)
|
|
||||||
|
|
||||||
rooms = (Room.objects.annotate(num_objects=Count('locations_set'))
|
|
||||||
.filter(num_objects__gte=5))
|
|
||||||
|
|
||||||
rooms = list(rooms)
|
|
||||||
|
|
||||||
```
|
|
||||||
Here we first create an annotation `num_objects` of type `Count`, which is a Django class. Note that
|
|
||||||
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*.
|
|
||||||
Once we have those, they are counted.
|
|
||||||
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 5. This is a little
|
|
||||||
harder to get one's head around but much more efficient than lopping over all objects in Python.
|
|
||||||
|
|
||||||
What if we wanted to compare two 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? Here an F-object comes in handy:
|
|
||||||
|
|
||||||
```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')))
|
|
||||||
```
|
|
||||||
|
|
||||||
F-objects allows for wrapping an annotated structure on the right-hand-side of the expression. It
|
|
||||||
will be evaluated on-the-fly as needed.
|
|
||||||
|
|
||||||
### Grouping By and Values
|
|
||||||
|
|
||||||
Suppose you used tags to mark someone belonging an organization. Now you want to make a list and
|
|
||||||
need to get the membership count of every organization all at once. That's where annotations and the
|
|
||||||
`.values_list` queryset method come in. Values/Values Lists are an alternate way of returning a
|
|
||||||
queryset - instead of objects, you get a list of dicts or tuples that hold selected properties from
|
|
||||||
the the matches. It also allows you a way to 'group up' queries for returning information. For
|
|
||||||
example, to get a display about each tag per Character and the names of the tag:
|
|
||||||
|
|
||||||
```python
|
|
||||||
result = (Character.objects.filter(db_tags__db_category="organization")
|
|
||||||
.values_list('db_tags__db_key')
|
|
||||||
.annotate(cnt=Count('id'))
|
|
||||||
.order_by('-cnt'))
|
|
||||||
```
|
|
||||||
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 Fanclub', 3872), ("Chainsol's Ainneve Testers", 2076), ("Blaufeuer's Whitespace Fixers",
|
|
||||||
1903),
|
|
||||||
("Volund's Bikeshed Design Crew", 1764), ("Tehom's Misanthropes", 1)]
|
|
||||||
129
docs/source/Howto/Starting/Part1/Evennia-Library-Overview.md
Normal file
129
docs/source/Howto/Starting/Part1/Evennia-Library-Overview.md
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
# Overview of the Evennia library
|
||||||
|
|
||||||
|
[prev lesson](Python-classes-and-objects) | [next lesson](Learning-Typeclasses)
|
||||||
|
|
||||||
|
```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).
|
||||||
|
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/Extended-Installation) 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) if you want to learn more about how this works.
|
||||||
|
|
||||||
|
This the the structure of the Evennia library:
|
||||||
|
|
||||||
|
- evennia
|
||||||
|
- [`__init__.py`](Evennia-API#shortcuts) - The "flat API" of Evennia resides here.
|
||||||
|
- [`settings_default.py`](Server-Conf#Settings-file) - Root settings of Evennia. Copy settings
|
||||||
|
from here to `mygame/server/settings.py` file.
|
||||||
|
- [`commands/`](Commands) - The command parser and handler.
|
||||||
|
- `default/` - The [default commands](../../../Component/Default-Command-Help) and cmdsets.
|
||||||
|
- [`comms/`](Communications) - 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/`](Help-System) - Handles the storage and creation of help entries.
|
||||||
|
- `locale/` - Language files ([i18n](../../../Concept/Internationalization)).
|
||||||
|
- [`locks/`](Locks) - Lock system for restricting access to in-game entities.
|
||||||
|
- [`objects/`](Objects) - In-game entities (all types of items and Characters).
|
||||||
|
- [`prototypes/`](Spawner-and-Prototypes) - Object Prototype/spawning system and OLC menu
|
||||||
|
- [`accounts/`](Accounts) - Out-of-game Session-controlled entities (accounts, bots etc)
|
||||||
|
- [`scripts/`](Scripts) - Out-of-game entities equivalence to Objects, also with timer support.
|
||||||
|
- [`server/`](Portal-And-Server) - Core server code and Session handling.
|
||||||
|
- `portal/` - Portal proxy and connection protocols.
|
||||||
|
- [`typeclasses/`](Typeclasses) - Abstract classes for the typeclass storage and database system.
|
||||||
|
- [`utils/`](Coding-Utils) - Various miscellaneous useful coding resources.
|
||||||
|
- [`web/`](Web-Features) - 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) 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 189](evennia/__init__.py#L189) 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#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)
|
||||||
|
2. Locate the link to [evennia.objects](api:evennia.objects) and click on it.
|
||||||
|
3. Click through to [evennia.objects.objects](api:evennia.objects.objects).
|
||||||
|
4. You are now in the python module. Scroll down (or search in your web browser) to find the `DefaultObject` class.
|
||||||
|
5. You can now read what this does and what methods are on it. If you want to see the full source, click the
|
||||||
|
\[[source](src:evennia.objects.objects#DefaultObject)\] link.
|
||||||
|
|
||||||
|
[prev lesson](Python-classes-and-objects) | [next lesson](Learning-Typeclasses)
|
||||||
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
# Persistent objects and typeclasses
|
# Persistent objects and typeclasses
|
||||||
|
|
||||||
[prev lesson](Python-classes-and-objects) | [next lesson](Adding-Commands)
|
[prev lesson](Evennia-Library-Overview) | [next lesson](Creating-Things)
|
||||||
|
|
||||||
In the last lesson we created the dragons Fluffy, Cuddly and Smaug and made the fly and breathe fire. We
|
Now that we have learned a little about how to find things in the Evennia library, let's use it.
|
||||||
learned a bit about _classes_ in the process. But so far our dragons are short-lived - whenever we `restart`
|
|
||||||
|
In the [Python classes and objects](Python-classes-and-objects) 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.
|
the server or `quit()` out of python mode they are gone.
|
||||||
|
|
||||||
This is what you should have in `mygame/typeclasses/monsters.py` so far:
|
This is what you should have in `mygame/typeclasses/monsters.py` so far:
|
||||||
|
|
@ -620,4 +622,4 @@ Typeclasses are a fundamental part of Evennia and we will see a lot of more uses
|
||||||
this tutorial. But that's enough of them for now. It's time to take some action. Let's learn about _Commands_.
|
this tutorial. But that's enough of them for now. It's time to take some action. Let's learn about _Commands_.
|
||||||
|
|
||||||
|
|
||||||
[prev lesson](Python-classes-and-objects) | [next lesson](Adding-Commands)
|
[prev lesson](Evennia-Library-Overview) | [next lesson](Creating-Things)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# More about Commands
|
# More about Commands
|
||||||
|
|
||||||
[prev lesson](Adding-Commands) | [next lesson](Evennia-API-Overview)
|
[prev lesson](Adding-Commands) | [next lesson](Creating-Things)
|
||||||
|
|
||||||
In this lesson we learn some basics about parsing the input of Commands. We will
|
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.
|
also learn how to add, modify and extend Evennia's default commands.
|
||||||
|
|
@ -497,8 +497,4 @@ In this lesson we got into some more advanced string formatting - many of those
|
||||||
the future! We also made a functional sword. Finally we got into how to add to, extend and replace a default
|
the future! We also made a functional sword. Finally we got into how to add to, extend and replace a default
|
||||||
command on ourselves.
|
command on ourselves.
|
||||||
|
|
||||||
In the last few lessons we have made use of resources from Evennia. Now that we have had some experience of how
|
[prev lesson](Adding-Commands) | [next lesson](Creating-Things)
|
||||||
classes and inheritance work, we can start exploring this in earnest.
|
|
||||||
|
|
||||||
|
|
||||||
[prev lesson](Adding-Commands) | [next lesson](Evennia-API-Overview)
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Python Classes and objects
|
# Python Classes and objects
|
||||||
|
|
||||||
[prev lesson](Gamedir-Overview) | [next lesson](Learning-Typeclasses)
|
[prev lesson](Gamedir-Overview) | [next lesson](Evennia-Library-Overview)
|
||||||
|
|
||||||
We have now learned how to run some simple Python code from inside (and outside) your game server.
|
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.
|
We have also taken a look at what our game dir looks and what is where. Now we'll start to use it.
|
||||||
|
|
@ -409,8 +409,7 @@ We have created our first dragons from classes. We have learned a little about h
|
||||||
into an _object_. We have seen some examples of _inheritance_ and we tested to _override_ a method in the parent
|
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.
|
with one in the child class. We also used `super()` to good effect.
|
||||||
|
|
||||||
But so far our dragons are gone as soon as we `restart` the server or `quit()` the Python interpreter. In the
|
We have used pretty much raw Python so far. In the coming lessons we'll start to look at the extra bits that Evennia
|
||||||
next lesson we'll get up close and personal with Smaug.
|
provides. But first we need to learn just where to find everything.
|
||||||
|
|
||||||
|
[prev lesson](Gamedir-Overview) | [next lesson](Evennia-Library-Overview)
|
||||||
[prev lesson](Gamedir-Overview) | [next lesson](Learning-Typeclasses)
|
|
||||||
|
|
|
||||||
576
docs/source/Howto/Starting/Part1/Searching-Things.md
Normal file
576
docs/source/Howto/Starting/Part1/Searching-Things.md
Normal file
|
|
@ -0,0 +1,576 @@
|
||||||
|
# Searching for things
|
||||||
|
|
||||||
|
[prev lesson](Creating-Things) | [next lesson]()
|
||||||
|
|
||||||
|
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 at
|
||||||
|
the end of this lesson.
|
||||||
|
```
|
||||||
|
|
||||||
|
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) 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.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](../../../Component/Objects)
|
||||||
|
- [Accounts](../../../Component/Accounts)
|
||||||
|
- [Scripts](../../../Component/Scripts),
|
||||||
|
- [Channels](../../../Component/Communications#channels),
|
||||||
|
- [Messages](Communication#Msg)
|
||||||
|
- [Help Entries](../../../Component/Help-System).
|
||||||
|
|
||||||
|
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](../../../Component/Tags) 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](../../../Component/Attributes) 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. We'll cover this some more as an [Extra-credits](#Extra-Credits) section at the end of this lesson.
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
|
||||||
|
## Database queries
|
||||||
|
|
||||||
|
The search functions and methods above 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'll immediately become 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
|
||||||
|
more efficient.
|
||||||
|
|
||||||
|
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. You won't find any `Cannon` instances in
|
||||||
|
the `all_weapon` result above, confusing as that may sound. To get instances of a Typeclass _and_ the
|
||||||
|
instances of all its children classes you need to use `_family`:
|
||||||
|
|
||||||
|
```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.
|
||||||
|
|
||||||
|
To actually 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 objects having a `db_key` equal to `"rose"`.
|
||||||
|
Since this is a queryset you can keep adding to it:
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
Only until we actually try to examine the result will the database be called. Here it's called when we
|
||||||
|
try to loop over the queryset:
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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, but when you access it in Python you can skip the `db_`. This
|
||||||
|
is why you can use `obj.key` and `obj.location` in normal code. Here we are calling the database directly though
|
||||||
|
and need to use the 'real' names.
|
||||||
|
|
||||||
|
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__iequals="rose")
|
||||||
|
|
||||||
|
The Django field query language uses `__` in the same way as 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"))
|
||||||
|
|
||||||
|
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 section.
|
||||||
|
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Line 3** - We want to find `Character`s, so we access `.objects` on the `Character` typeclass.
|
||||||
|
- **Line 4** - We start to filter ...
|
||||||
|
- **Line 5**
|
||||||
|
- ... 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).
|
||||||
|
- **Line 6** - ... We also want only Characters with `Attributes` whose `db_key` is exactly `"lycantrophy"`
|
||||||
|
- **Line 7** - ... at the same time as the `Attribute`'s `db_value` is greater-than 2.
|
||||||
|
|
||||||
|
Running this query makes our newly lycantrrophic Character appear in `will_transform`. Success!
|
||||||
|
|
||||||
|
> Don't confuse database fields with [Attributes](../../../Component/Attributes) 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
|
||||||
|
import from Django directly:
|
||||||
|
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
`Q()` objects take the same arguments like `.filter`:
|
||||||
|
|
||||||
|
Q(db_key="foo")
|
||||||
|
|
||||||
|
The special thing is that these `Q` objects can then be chained together with special symbols:
|
||||||
|
`|` for `OR`, `&` for `AND`. A tilde `~` in front negates the expression inside the `Q` and thus works like `NOT`.
|
||||||
|
|
||||||
|
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!). Let's say there is
|
||||||
|
a Tag "recently_bitten" that controls 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")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
We now grouped the filter
|
||||||
|
|
||||||
|
In our original Lycanthrope example we wanted our werewolves to have names that could start with any
|
||||||
|
vowel except for the specific beginning "ab".
|
||||||
|
|
||||||
|
```python
|
||||||
|
from django.db.models import Q
|
||||||
|
from typeclasses.characters import Character
|
||||||
|
|
||||||
|
query = Q()
|
||||||
|
for letter in ("aeiouy"):
|
||||||
|
query |= Q(db_key__istartswith=letter)
|
||||||
|
query &= ~Q(db_key__istartswith="ab")
|
||||||
|
query = Character.objects.filter(query)
|
||||||
|
|
||||||
|
list_of_lycanthropes = list(query)
|
||||||
|
```
|
||||||
|
|
||||||
|
In the above example, we construct our query our of several Q objects that each represent one part
|
||||||
|
of the query. We iterate over the list of vowels, and add an `OR` condition to the query using `|=`
|
||||||
|
(this is the same idea as using `+=` which may be more familiar). Each `OR` condition checks that
|
||||||
|
the name starts with one of the valid vowels. Afterwards, we add (using `&=`) an `AND` condition
|
||||||
|
that is negated with the `~` symbol. In other words we require that any match should *not* start
|
||||||
|
with the string "ab". Note that we don't actually hit the database until we convert the query to a
|
||||||
|
list at the end (we didn't need to do that either, but could just have kept the query until we
|
||||||
|
needed to do something with the matches).
|
||||||
|
|
||||||
|
### Annotations and `F` objects
|
||||||
|
|
||||||
|
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* retrieve all interesting candidates and run them through a for-loop to get and count
|
||||||
|
their `.content` properties. We'd then just return a list of only those objects with enough
|
||||||
|
contents. It would look something like this (note: don't actually do this!):
|
||||||
|
|
||||||
|
```python
|
||||||
|
# probably not a good idea to do it this way
|
||||||
|
|
||||||
|
from typeclasses.rooms import Room
|
||||||
|
|
||||||
|
queryset = Room.objects.all() # get all Rooms
|
||||||
|
rooms = [room for room in queryset if len(room.contents) >= 5]
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Once the number of rooms in your game increases, this could become quite expensive. Additionally, in
|
||||||
|
some particular contexts, like when using the web features of Evennia, you must have the result as a
|
||||||
|
queryset in order to use it in operations, such as in Django's admin interface when creating list
|
||||||
|
filters.
|
||||||
|
|
||||||
|
Enter [F objects](https://docs.djangoproject.com/en/1.11/ref/models/expressions/#f-expressions) and
|
||||||
|
*annotations*. So-called F expressions allow you to do a query that looks at a value of each object
|
||||||
|
in the database, while annotations allow you to calculate and attach a value to a query. So, let's
|
||||||
|
do the same example as before directly in the database:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typeclasses.rooms import Room
|
||||||
|
from django.db.models import Count
|
||||||
|
|
||||||
|
room_count = Room.objects.annotate(num_objects=Count('locations_set'))
|
||||||
|
queryset = room_count.filter(num_objects__gte=5)
|
||||||
|
|
||||||
|
rooms = (Room.objects.annotate(num_objects=Count('locations_set'))
|
||||||
|
.filter(num_objects__gte=5))
|
||||||
|
|
||||||
|
rooms = list(rooms)
|
||||||
|
|
||||||
|
```
|
||||||
|
Here we first create an annotation `num_objects` of type `Count`, which is a Django class. Note that
|
||||||
|
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*.
|
||||||
|
Once we have those, they are counted.
|
||||||
|
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 5. This is a little
|
||||||
|
harder to get one's head around but much more efficient than lopping over all objects in Python.
|
||||||
|
|
||||||
|
What if we wanted to compare two 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? Here an F-object comes in handy:
|
||||||
|
|
||||||
|
```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')))
|
||||||
|
```
|
||||||
|
|
||||||
|
F-objects allows for wrapping an annotated structure on the right-hand-side of the expression. It
|
||||||
|
will be evaluated on-the-fly as needed.
|
||||||
|
|
||||||
|
### Grouping By and Values
|
||||||
|
|
||||||
|
Suppose you used tags to mark someone belonging an organization. Now you want to make a list and
|
||||||
|
need to get the membership count of every organization all at once. That's where annotations and the
|
||||||
|
`.values_list` queryset method come in. Values/Values Lists are an alternate way of returning a
|
||||||
|
queryset - instead of objects, you get a list of dicts or tuples that hold selected properties from
|
||||||
|
the the matches. It also allows you a way to 'group up' queries for returning information. For
|
||||||
|
example, to get a display about each tag per Character and the names of the tag:
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = (Character.objects.filter(db_tags__db_category="organization")
|
||||||
|
.values_list('db_tags__db_key')
|
||||||
|
.annotate(cnt=Count('id'))
|
||||||
|
.order_by('-cnt'))
|
||||||
|
```
|
||||||
|
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 Fanclub', 3872), ("Chainsol's Ainneve Testers", 2076), ("Blaufeuer's Whitespace Fixers",
|
||||||
|
1903),
|
||||||
|
("Volund's Bikeshed Design Crew", 1764), ("Tehom's Misanthropes", 1)]
|
||||||
|
|
||||||
|
[prev lesson](Creating-Things) | [next lesson]()
|
||||||
|
|
@ -132,7 +132,7 @@ Talker-type game you *will* have to bite the bullet and code your game (or find
|
||||||
do it for you).
|
do it for you).
|
||||||
|
|
||||||
Even if you won't code anything yourself, as a designer you need to at least understand the basic
|
Even if you won't code anything yourself, as a designer you need to at least understand the basic
|
||||||
paradigms of Evennia, such as [Objects](../../Component/Objects), [Commands](../../Component/Commands) and [Scripts](../../Component/Scripts) and
|
paradigms of Evennia, such as [Objects](../../../Component/Objects), [Commands](../../../Component/Commands) and [Scripts](../../../Component/Scripts) and
|
||||||
how they hang together. We recommend you go through the [Tutorial World](Tutorial-World-
|
how they hang together. We recommend you go through the [Tutorial World](Tutorial-World-
|
||||||
Introduction) in detail (as well as glancing at its code) to get at least a feel for what is
|
Introduction) in detail (as well as glancing at its code) to get at least a feel for what is
|
||||||
involved behind the scenes. You could also look through the tutorial for [building a game from
|
involved behind the scenes. You could also look through the tutorial for [building a game from
|
||||||
|
|
@ -144,7 +144,7 @@ The earlier you revise problems, the easier they will be to fix.
|
||||||
|
|
||||||
A good idea is to host your code online (publicly or privately) using version control. Not only will
|
A good idea is to host your code online (publicly or privately) using version control. Not only will
|
||||||
this make it easy for multiple coders to collaborate (and have a bug-tracker etc), it also means
|
this make it easy for multiple coders to collaborate (and have a bug-tracker etc), it also means
|
||||||
your work is backed up at all times. The [Version Control](../../Coding/Version-Control) tutorial has
|
your work is backed up at all times. The [Version Control](../../../Coding/Version-Control) tutorial has
|
||||||
instructions for setting up a sane developer environment with proper version control.
|
instructions for setting up a sane developer environment with proper version control.
|
||||||
|
|
||||||
### "Tech Demo" Building
|
### "Tech Demo" Building
|
||||||
|
|
@ -199,7 +199,7 @@ flag and let people try it! Call upon your alpha-players to try everything - the
|
||||||
to break your game in ways that you never could have imagined. In Alpha you might be best off to
|
to break your game in ways that you never could have imagined. In Alpha you might be best off to
|
||||||
focus on inviting friends and maybe other MUD developers, people who you can pester to give proper
|
focus on inviting friends and maybe other MUD developers, people who you can pester to give proper
|
||||||
feedback and bug reports (there *will* be bugs, there is no way around it). Follow the quick
|
feedback and bug reports (there *will* be bugs, there is no way around it). Follow the quick
|
||||||
instructions for [Online Setup](../../Setup/Online-Setup) to make your game visible online. If you hadn't
|
instructions for [Online Setup](../../../Setup/Online-Setup) to make your game visible online. If you hadn't
|
||||||
already, make sure to put up your game on the [Evennia game index](http://games.evennia.com/) so
|
already, make sure to put up your game on the [Evennia game index](http://games.evennia.com/) so
|
||||||
people know it's in the works (actually, even pre-alpha games are allowed in the index so don't be
|
people know it's in the works (actually, even pre-alpha games are allowed in the index so don't be
|
||||||
shy)!
|
shy)!
|
||||||
|
|
@ -27,11 +27,12 @@ own first little game in Evennia. Let's get started!
|
||||||
1. [Python basics](Part1/Python-basic-introduction)
|
1. [Python basics](Part1/Python-basic-introduction)
|
||||||
1. [Game dir overview](Part1/Gamedir-Overview)
|
1. [Game dir overview](Part1/Gamedir-Overview)
|
||||||
1. [Python classes and objects](Part1/Python-classes-and-objects)
|
1. [Python classes and objects](Part1/Python-classes-and-objects)
|
||||||
1. [Persistent objects](Part1/Learning-Typeclasses)
|
1. [Accessing the Evennia library](Part1/Evennia-Library-Overview)
|
||||||
|
1. [Typeclasses - Persistent objects](Part1/Learning-Typeclasses)
|
||||||
1. [Making our first own commands](Part1/Adding-Commands)
|
1. [Making our first own commands](Part1/Adding-Commands)
|
||||||
1. [Parsing and replacing default Commands](Part1/More-on-Commands)
|
1. [Parsing and replacing default Commands](Part1/More-on-Commands)
|
||||||
1. [Searching and creating things](Tutorial-Searching-For-Objects)
|
1. [Creating things](Part1/Creating-Things)
|
||||||
1. [A walkthrough of the API](Walkthrough-of-API)
|
1. [Searching for things](Part1/Searching-Things)
|
||||||
|
|
||||||
In this first part we'll focus on what we get out of the box in Evennia - we'll get used to the tools,
|
In this first part we'll focus on what we get out of the box in Evennia - we'll get used to the tools,
|
||||||
where things are and how we find things we are looking for. We will also dive into some of things you'll
|
where things are and how we find things we are looking for. We will also dive into some of things you'll
|
||||||
|
|
|
||||||
|
|
@ -92,22 +92,23 @@
|
||||||
- [Howto/Manually Configuring Color](Howto/Manually-Configuring-Color)
|
- [Howto/Manually Configuring Color](Howto/Manually-Configuring-Color)
|
||||||
- [Howto/Mass and weight for objects](Howto/Mass-and-weight-for-objects)
|
- [Howto/Mass and weight for objects](Howto/Mass-and-weight-for-objects)
|
||||||
- [Howto/NPC shop Tutorial](Howto/NPC-shop-Tutorial)
|
- [Howto/NPC shop Tutorial](Howto/NPC-shop-Tutorial)
|
||||||
- [Howto/Starting/API Overview](Howto/Starting/API-Overview)
|
|
||||||
- [Howto/Starting/Add a simple new web page](Howto/Starting/Add-a-simple-new-web-page)
|
- [Howto/Starting/Add a simple new web page](Howto/Starting/Add-a-simple-new-web-page)
|
||||||
- [Howto/Starting/Coordinates](Howto/Starting/Coordinates)
|
- [Howto/Starting/Coordinates](Howto/Starting/Coordinates)
|
||||||
- [Howto/Starting/First Steps Coding](Howto/Starting/First-Steps-Coding)
|
- [Howto/Starting/First Steps Coding](Howto/Starting/First-Steps-Coding)
|
||||||
- [Howto/Starting/Game Planning](Howto/Starting/Game-Planning)
|
|
||||||
- [Howto/Starting/Implementing a game rule system](Howto/Starting/Implementing-a-game-rule-system)
|
- [Howto/Starting/Implementing a game rule system](Howto/Starting/Implementing-a-game-rule-system)
|
||||||
- [Howto/Starting/Parsing command arguments, theory and best practices](Howto/Starting/Parsing-command-arguments,-theory-and-best-practices)
|
- [Howto/Starting/Parsing command arguments, theory and best practices](Howto/Starting/Parsing-command-arguments,-theory-and-best-practices)
|
||||||
- [Howto/Starting/Part1/Adding Commands](Howto/Starting/Part1/Adding-Commands)
|
- [Howto/Starting/Part1/Adding Commands](Howto/Starting/Part1/Adding-Commands)
|
||||||
- [Howto/Starting/Part1/Building Quickstart](Howto/Starting/Part1/Building-Quickstart)
|
- [Howto/Starting/Part1/Building Quickstart](Howto/Starting/Part1/Building-Quickstart)
|
||||||
- [Howto/Starting/Part1/Evennia API Overview](Howto/Starting/Part1/Evennia-API-Overview)
|
- [Howto/Starting/Part1/Creating Things](Howto/Starting/Part1/Creating-Things)
|
||||||
|
- [Howto/Starting/Part1/Evennia Library Overview](Howto/Starting/Part1/Evennia-Library-Overview)
|
||||||
- [Howto/Starting/Part1/Gamedir Overview](Howto/Starting/Part1/Gamedir-Overview)
|
- [Howto/Starting/Part1/Gamedir Overview](Howto/Starting/Part1/Gamedir-Overview)
|
||||||
- [Howto/Starting/Part1/Learning Typeclasses](Howto/Starting/Part1/Learning-Typeclasses)
|
- [Howto/Starting/Part1/Learning Typeclasses](Howto/Starting/Part1/Learning-Typeclasses)
|
||||||
- [Howto/Starting/Part1/More on Commands](Howto/Starting/Part1/More-on-Commands)
|
- [Howto/Starting/Part1/More on Commands](Howto/Starting/Part1/More-on-Commands)
|
||||||
- [Howto/Starting/Part1/Python basic introduction](Howto/Starting/Part1/Python-basic-introduction)
|
- [Howto/Starting/Part1/Python basic introduction](Howto/Starting/Part1/Python-basic-introduction)
|
||||||
- [Howto/Starting/Part1/Python classes and objects](Howto/Starting/Part1/Python-classes-and-objects)
|
- [Howto/Starting/Part1/Python classes and objects](Howto/Starting/Part1/Python-classes-and-objects)
|
||||||
|
- [Howto/Starting/Part1/Searching Things](Howto/Starting/Part1/Searching-Things)
|
||||||
- [Howto/Starting/Part1/Tutorial World Introduction](Howto/Starting/Part1/Tutorial-World-Introduction)
|
- [Howto/Starting/Part1/Tutorial World Introduction](Howto/Starting/Part1/Tutorial-World-Introduction)
|
||||||
|
- [Howto/Starting/Part2/Game Planning](Howto/Starting/Part2/Game-Planning)
|
||||||
- [Howto/Starting/Starting Part1](Howto/Starting/Starting-Part1)
|
- [Howto/Starting/Starting Part1](Howto/Starting/Starting-Part1)
|
||||||
- [Howto/Starting/Starting Part2](Howto/Starting/Starting-Part2)
|
- [Howto/Starting/Starting Part2](Howto/Starting/Starting-Part2)
|
||||||
- [Howto/Starting/Starting Part3](Howto/Starting/Starting-Part3)
|
- [Howto/Starting/Starting Part3](Howto/Starting/Starting-Part3)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue