Typeclasses form the core of Evennia data storage. It allows Evennia to represent any number of different game entities as Python classes, without having to modify the database schema for every new type.
In Evennia the most important game entities, Accounts, Objects,
Scripts and Channels are all Python classes inheriting, at varying
evennia.typeclasses.models.TypedObject. In the
documentation we refer to these objects as being “typeclassed” or even
“being a typeclass”.
This is how the inheritance looks for the typeclasses in Evennia:
TypedObject _________________|_________________________________ | | | | 1: AccountDB ObjectDB ScriptDB ChannelDB | | | | 2: DefaultAccount DefaultObject DefaultScript DefaultChannel | DefaultCharacter | | | DefaultRoom | | | DefaultExit | | | | | | 3: Account Object Script Channel Character Room Exit
- Level 1 above is the “database model” level. This describes the database tables and fields (this is technically a Django model).
- Level 2 is where we find Evennia’s default implementations of the
various game entities, on top of the database. These classes define
all the hook methods that Evennia calls in various situations.
DefaultObjectis a little special since it’s the parent for
DefaultExit. They are all grouped under level 2 because they all represents defaults to build from.
- Level 3, finally, holds empty template classes created in your
game directory. This is the level you are meant to modify and tweak
as you please, overloading the defaults as befits your game. The
templates inherit directly from their defaults, so
typeclass/listcommand will provide a list of all typeclasses known to
typeclass/listwill not find it. To make it known
Difference between typeclasses and classes¶
All Evennia classes inheriting from class in the table above share one important feature and two important limitations. This is why we don’t simply call them “classes” but “typeclasses”.
A typeclass can save itself to the database. This means that some properties (actually not that many) on the class actually represents database fields and can only hold very specific data types. This is detailed below.
Due to its connection to the database, the typeclass’ name must be unique across the entire server namespace. That is, there must never be two same-named classes defined anywhere. So the below code would give an error (since
DefaultObjectis now globally found both in this module and in the default library):
from evennia import DefaultObject as BaseObject class DefaultObject(BaseObject): pass
__init__method should normally not be overloaded. This has mostly to do with the fact that the
__init__method is not called in a predictable way. Instead Evennia suggest you use the
at_object_creationfor Objects) for setting things the very first time the typeclass is saved to the database or the
at_inithook which is called every time the object is cached to memory. If you know what you are doing and want to use
__init__, it must both accept arbitrary keyword arguments and use
superto call its parent:
.. code:: python
Creating a new typeclass¶
It’s easy to work with Typeclasses. Either you use an existing typeclass or you create a new Python class inheriting from an existing typeclass. Here is an example of creating a new type of Object:
from evennia import DefaultObject class Furniture(DefaultObject): # this defines what 'furniture' is, like # storing who sits on it or something. pass
You can now create a new
Furniture object in two ways. First (and
usually not the most convenient) way is to create an instance of the
class and then save it manually to the database:
chair = Furniture(db_key="Chair") chair.save()
To use this you must give the database field names as keywords to the
call. Which are available depends on the entity you are creating, but
all start with
db_* in Evennia. This is a method you may be familiar
with if you know Django from before.
It is recommended that you instead use the
create_* functions to
create typeclassed entities:
from evennia import create_object chair = create_object(Furniture, key="Chair") # or (if your typeclass is in a module furniture.py) chair = create_object("furniture.Furniture", key="Chair")
create_script etc) takes
the typeclass as its first argument; this can both be the actual class
or the python path to the typeclass as found under your game directory.
So if your
Furniture typeclass sits in
mygame/typeclasses/furniture.py, you could point to it as
typeclasses.furniture.Furniture. Since Evennia will itself look in
mygame/typeclasses, you can shorten this even further to just
furniture.Furniture. The create-functions take a lot of extra
keywords allowing you to set things like Attributes and Tags all
in one go. These keywords don’t use the
db_* prefix. This will also
automatically save the new instance to the database, so you don’t need
About typeclass properties¶
An example of a database field is
db_key. This stores the “name” of
the entity you are modifying and can thus only hold a string. This is
one way of making sure to update the
chair.db_key = "Table" chair.save() print(chair.db_key) <<< Table
That is, we change the chair object to have the
db_key “Table”, then
save this to the database. However, you almost never do things this way;
Evennia defines property wrappers for all the database fields. These are
named the same as the field, but without the
chair.key = "Table" print(chair.key) <<< Table
key wrapper is not only shorter to write, it will make sure to
save the field for you, and does so more efficiently by levering sql
update mechanics under the hood. So whereas it is good to be aware that
the field is named
db_key you should use
key as much as you can.
Each typeclass entity has some unique fields relevant to that type. But
all also share the following fields (the wrapper name without
key(str): The main identifier for the entity, like “Rose”, “myscript” or “Paul”.
nameis an alias.
date_created(datetime): Time stamp when this object was created.
typeclass_path(str): A python path pointing to the location of this (type)class
There is one special field that doesn’t use the
db_ prefix (it’s
defined by Django):
id(int): the database id (database ref) of the object. This is an ever-increasing, unique integer. It can also be accessed as
dbid(database ID) or
pk(primary key). The
dbrefproperty returns the string form “#id”.
The typeclassed entity has several common handlers:
tags- the TagHandler that handles tagging. Use
locks- the LockHandler that manages access restrictions. Use
attributes- the AttributeHandler that manages Attributes on the object. Use
db(DataBase) - a shortcut property to the AttributeHandler; allowing
obj.db.attrname = value
nattributes- the Non-persistent AttributeHandler for attributes not saved in the database.
ndb(NotDataBase) - a shortcut property to the Non-peristent AttributeHandler. Allows
obj.ndb.attrname = value
Each of the typeclassed entities then extend this list with their own properties. Go to the respective pages for Objects, Scripts, Accounts and Channels for more info. It’s also recommended that you explore the available entities using Evennia’s flat API to explore which properties and methods they have available.
The way to customize typeclasses is usually to overload hook methods
on them. Hooks are methods that Evennia call in various situations. An
example is the
at_object_creation hook on
Objects, which is only
called once, the very first time this object is saved to the database.
Other examples are the
at_login hook of Accounts and the
at_repeat hook of Scripts.
Querying for typeclasses¶
Most of the time you search for objects in the database by using
convenience methods like the
caller.search() of Commands or the
search functions like
You can however also query for them directly using Django’s query
language. This makes use of a database manager that sits on all
objects. This manager holds methods that allow
database searches against that particular type of object (this is the
way Django normally works too). When using Django queries, you need to
use the full field names (like
db_key) to search:
matches = Furniture.objects.get(db_key="Chair")
It is important that this will only find objects inheriting directly
Furniture in your database. If there was a subclass of
Sitables you would not find any chairs derived
Sitables with this query (this is not a Django feature but
special to Evennia). To find objects from subclasses Evennia instead
filter_family query methods available:
# search for all furnitures and subclasses of furnitures # whose names starts with "Chair" matches = Furniture.objects.filter_family(db_key__startswith="Chair")
To make sure to search, say, all
Scripts regardless of typeclass,
you need to query from the database model itself. So for Objects, this
ObjectDB in the diagram above. Here’s an example for
from evennia import ScriptDB matches = ScriptDB.objects.filter(db_key__contains="Combat")
When querying from the database model parent you don’t need to use
get_family - you will always query all children
on the database model.
Updating existing typeclass instances¶
If you already have created instances of Typeclasses, you can modify the Python code at any time - due to how Python inheritance works your changes will automatically be applied to all children once you have reloaded the server.
However, database-saved data, like
db_* fields, Attributes,
Tags etc, are not themselves embedded into the class and will not
be updated automatically. This you need to manage yourself, by searching
for all relevant objects and updating or adding the data:
# add a worth Attribute to all existing Furniture for obj in Furniture.objects.all(): # this will loop over all Furniture instances obj.db.worth = 100
A common use case is putting all Attributes in the
hook of the entity, such as
is called every time an object is created - and only then. This is
usually what you want but it does mean already existing objects won’t
get updated if you change the contents of
You can fix this in a similar way as above (manually setting each
Attribute) or with something like this:
# Re-run at_object_creation only on those objects not having the new Attribute for obj in Furniture.objects.all(): if not obj.db.worth: obj.at_object_creation()
The above examples can be run in the command prompt created by
evennia shell. You could also run it all in-game using
however requires you to put the code (including imports) as one single
; and list comprehensions, like this (ignore the line
break, that’s only for readability in the wiki):
@py from typeclasses.furniture import Furniture; [obj.at_object_creation() for obj in Furniture.objects.all() if not obj.db.worth]
It is recommended that you plan your game properly before starting to build, to avoid having to retroactively update objects more than necessary.
If you want to swap an already existing typeclass, there are two ways to
do so: From in-game and via code. From inside the game you can use the
@typeclass objname = path.to.new.typeclass
There are two important switches to this command:
/reset- This will purge all existing Attributes on the object and re-run the creation hook (like
at_object_creationfor Objects). This assures you get an object which is purely of this new class.
/force- This is required if you are changing the class to be the same class the object already has - it’s a safety check to avoid user errors. This is usually used together with
/resetto re-run the creation hook on an existing class.
In code you instead use the
swap_typeclass method which you can find
on all typeclassed entities:
obj_to_change.swap_typeclass(new_typeclass_path, clean_attributes=False, run_start_hooks="all", no_default=True, clean_cmdsets=False)
The arguments to this method are described in the API docs here.
How typeclasses actually work¶
This is considered an advanced section.
Technically, typeclasses are Django proxy models. The only database
models that are “real” in the typeclass system (that is, are represented
by actual tables in the database) are
ChannelDB (there are also Attributes and Tags
but they are not typeclasses themselves). All the subclasses of them are
“proxies”, extending them with Python code without actually modifying
the database layout.
Evennia modifies Django’s proxy model in various ways to allow them to
work without any boiler plate (for example you don’t need to set the
Django “proxy” property in the model
Meta subclass, Evennia handles
this for you using metaclasses). Evennia also makes sure you can query
subclasses as well as patches django to allow multiple inheritance from
the same base class.
Evennia uses the idmapper to cache its typeclasses (Django proxy models) in memory. The idmapper allows things like on-object handlers and properties to be stored on typeclass instances and to not get lost as long as the server is running (they will only be cleared on a Server reload). Django does not work like this by default; by default every time you search for an object in the database you’ll get a different instance of that object back and anything you stored on it that was not in the database would be lost. The bottom line is that Evennia’s Typeclass instances subside in memory a lot longer than vanilla Django model instance do.
There is one caveat to consider with this, and that relates to making your own models: Foreign relationships to typeclasses are cached by Django and that means that if you were to change an object in a foreign relationship via some other means than via that relationship, the object seeing the relationship may not reliably update but will still see its old cached version. Due to typeclasses staying so long in memory, stale caches of such relationships could be more visible than common in Django. See the closed issue #1098 and its comments for examples and solutions.