7. Making objects persistent¶
Now that we have learned a little about how to find things in the Evennia library, let’s use it.
In the Python classes and objects lesson we created the dragons Fluffy, Cuddly
and Smaug and made them fly and breathe fire. So far our dragons are short-lived - whenever we restart
the server or quit()
out of python mode they are gone.
This is what you should have in mygame/typeclasses/monsters.py
so far:
class Monster:
"""
This is a base class for Monsters.
"""
def __init__(self, key):
self.key = key
def move_around(self):
print(f"{self.key} is moving!")
class Dragon(Monster):
"""
This is a dragon-specific monster.
"""
def move_around(self):
super().move_around()
print("The world trembles.")
def firebreath(self):
"""
Let our dragon breathe fire.
"""
print(f"{self.key} breathes fire!")
7.1. Our first persistent object¶
At this point we should know enough to understand what is happening in mygame/typeclasses/objects.py
. Let’s
open it:
"""
module docstring
"""
from evennia import DefaultObject
class ObjectParent:
"""
class docstring
"""
pass
class Object(ObjectParent, DefaultObject):
"""
class docstring
"""
pass
So we have a class Object
that inherits from ObjectParent
(which is empty) and DefaultObject
, which we have imported from Evennia. The ObjectParent
acts as a place to put code you want all
of your Objects
to have. We’ll focus on Object
and DefaultObject
for now.
The class itself doesn’t do anything (it just pass
es) but that doesn’t mean it’s useless. As we’ve seen, it inherits all the functionality of its parent. It’s in fact an exact replica of DefaultObject
right now. Once we know what kind of methods and resources are available on DefaultObject
we could add our own and change the way it works!
One thing that Evennia classes offers and which you don’t get with vanilla Python classes is persistence - they survive a server reload since they are stored in the database.
Go back to mygame/typeclasses/monsters.py
. Change it as follows:
from typeclasses.objects import Object
class Monster(Object):
"""
This is a base class for Monsters.
"""
def move_around(self):
print(f"{self.key} is moving!")
class Dragon(Monster):
"""
This is a dragon-specific Monster.
"""
def move_around(self):
super().move_around()
print("The world trembles.")
def firebreath(self):
"""
Let our dragon breathe fire.
"""
print(f"{self.key} breathes fire!")
Don’t forget to save. We removed Monster.__init__
and made Monster
inherit from Evennia’s Object
(which in turn inherits from Evennia’s DefaultObject
, as we saw). By extension, this means that Dragon
also inherits from DefaultObject
, just from further away!
7.1.1. Making a new object by calling the class¶
First reload the server as usual. We will need to create the dragon a little differently this time:
> py
> from typeclasses.monsters import Dragon
> smaug = Dragon(db_key="Smaug", db_location=here)
> smaug.save()
> smaug.move_around()
Smaug is moving!
The world trembles.
Smaug works the same as before, but we created him differently: first we used
Dragon(db_key="Smaug", db_location=here)
to create the object, and then we used smaug.save()
afterwards.
> quit()
Python Console is closing.
> look
You should now see that Smaug is in the room with you. Woah!
> reload
> look
He’s still there… What we just did was to create a new entry in the database for Smaug. We gave the object its name (key) and set its location to our current location.
To make use of Smaug in code we must first find him in the database. For an object in the current
location we can easily do this in py
by using me.search()
:
> py smaug = me.search("Smaug") ; smaug.firebreath()
Smaug breathes fire!
7.1.2. Creating using create_object¶
Creating Smaug like we did above is nice because it’s similar to how we created non-database
bound Python instances before. But you need to use db_key
instead of key
and you also have to
remember to call .save()
afterwards. Evennia has a helper function that is more common to use,
called create_object
. Let’s recreate Cuddly this time:
> py evennia.create_object('typeclasses.monsters.Monster', key="Cuddly", location=here)
> look
Boom, Cuddly should now be in the room with you, a little less scary than Smaug. You specify the
python-path to the code you want and then set the key and location (if you had the Monster
class already imported, you could have passed that too). Evennia sets things up and saves for you.
If you want to find Smaug from anywhere (not just in the same room), you can use Evennia’s search_object
function:
> cuddly = evennia.search_object("Cuddly")[0] ; cuddly.move_around()
Cuddly is moving!
The
[0]
is becausesearch_object
always returns a list of zero, one or more found objects. The[0]
means that we want the first element of this list (counting in Python always starts from 0). If there were multiple Cuddlies we could get the second one with[1]
.
7.1.3. Creating using create-command¶
Finally, you can also create a new dragon using the familiar builder-commands we explored a few lessons ago:
> create/drop Fluffy:typeclasses.monsters.Dragon
Fluffy is now in the room. After learning about how objects are created you’ll realize that all this command really does is to parse your input, figure out that /drop
means to “give the object the same location as the caller”, and then do a call very similar to
evennia.create_object("typeclasses.monsters.Dragon", key="Cuddly", location=here)
That’s pretty much all there is to the mighty create
command! The rest is just parsing for the command to understand just what the user wants to create.
7.2. Typeclasses¶
The Object
(and DefafultObject
class we inherited from above is what we refer to as a Typeclass. This is an Evennia thing. The instance of a typeclass saves itself to the database when it is created, and after that you can just search for it to get it back.
We use the term typeclass or typeclassed to differentiate these types of classes and objects from the normal Python classes, whose instances go away on a reload.
The number of typeclasses in Evennia are so few they can be learned by heart:
Evennia base typeclass |
mygame.typeclasses child |
description |
---|---|---|
|
|
Everything with a location |
|
|
Player avatars |
|
|
In-game locations |
|
|
Links between rooms |
|
|
A player account |
|
|
In-game comms |
|
|
Entities with no location |
The child classes under mygame/typeclasses/
are meant for you to conveniently modify and
work with. Every class inheriting (at any distance) from a Evennia base typeclass is also considered a typeclass.
from somewhere import Something
from evennia import DefaultScript
class MyOwnClass(Something):
# not inheriting from an Evennia core typeclass, so this
# is just a 'normal' Python class inheriting from somewhere
pass
class MyOwnClass2(DefaultScript):
# inherits from one of the core Evennia typeclasses, so
# this is also considered a 'typeclass'.
pass
Notice that the classes in mygame/typeclasses/
are not inheriting from each other. For example, Character
is inheriting from evennia.DefaultCharacter
and not from typeclasses.objects.Object
. So if you change Object
you will not cause any change in the Character
class. If you want that you can easily just change the child classes to inherit in that way instead; Evennia doesn’t care.
As seen with our Dragon
example, you don’t have to modify these modules directly. You can just make your own modules and import the base class.
7.2.1. Examining objects¶
When you do
> create/drop giantess:typeclasses.monsters.Monster
You create a new Monster: giantess.
or
> py evennia.create_object("typeclasses.monsters.Monster", key="Giantess", location=here)
You are specifying exactly which typeclass you want to use to build the Giantess. Let’s examine the result:
> examine giantess
-------------------------------------------------------------------------------
Name/key: Giantess (#14)
Typeclass: Monster (typeclasses.monsters.Monster)
Location: Limbo (#2)
Home: Limbo (#2)
Permissions: <None>
Locks: call:true(); control:id(1) or perm(Admin); delete:id(1) or perm(Admin);
drop:holds(); edit:perm(Admin); examine:perm(Builder); get:all();
puppet:pperm(Developer); tell:perm(Admin); view:all()
Persistent attributes:
desc = You see nothing special.
-------------------------------------------------------------------------------
We used the examine
command briefly in the lesson about building in-game. Now these lines
may be more useful to us:
Name/key - The name of this thing. The value
(#14)
is probably different for you. This is the unique ‘primary key’ or dbref for this entity in the database.Typeclass: This show the typeclass we specified, and the path to it.
Location: We are in Limbo. If you moved elsewhere you’ll see that instead. Also the
#dbref
of Limbo is shown.Home: All objects with a location (inheriting from
DefaultObject
) must have a home location. This is a backup to move the object to if its current location is deleted.Permissions: Permissions are like the inverse to Locks - they are like keys to unlock access to other things. The giantess have no such keys (maybe fortunately). The Permissions has more info.
Locks: Locks are the inverse of Permissions - specify what criterion other objects must fulfill in order to access the
giantess
object. This uses a very flexible mini-language. For examine, the lineexamine:perm(Builders)
is read as “Only those with permission Builder or higher can examine this object”. Since we are the superuser we pass (even bypass) such locks with ease. See the Locks documentation for more info.Persistent attributes: This allows for storing arbitrary, persistent data on the typeclassed entity. We’ll get to those in the next section.
Note how the Typeclass line describes exactly where to find the code of this object? This is very useful for understanding how any object in Evennia works.
7.2.2. Default typeclasses¶
What happens if we create an object and don’t specify its typeclass though?
> create/drop box
You create a new Object: box.
or
> py create.create_object(None, key="box", location=here)
Now check it out:
> examine box
You will find that the Typeclass line now reads
Typeclass: Object (typeclasses.objects.Object)
So when you didn’t specify a typeclass, Evennia used a default, more specifically the (so far) empty Object
class in mygame/typeclasses/objects.py
. This is usually what you want, especially since you can tweak that class as much as you like.
But the reason Evennia knows to fall back to this class is not hard-coded - it’s a setting. The default is in evennia/settings_default.py, with the name BASE_OBJECT_TYPECLASS
, which is set to typeclasses.objects.Object
.
So if you wanted the creation commands and methods to default to some other class you could
add your own BASE_OBJECT_TYPECLASS
line to mygame/server/conf/settings.py
. The same is true for all the other typeclasseses, like characters, rooms and accounts. This way you can change the layout of your game dir considerably if you wanted. You just need to tell Evennia where everything is.
7.3. Modifying ourselves¶
Let’s try to modify ourselves a little. Open up mygame/typeclasses/characters.py
.
"""
(module docstring)
"""
from evennia import DefaultCharacter
class Character(DefaultCharacter):
"""
(class docstring)
"""
pass
This looks quite familiar now - an empty class inheriting from the Evennia base typeclass (it’s even easier than Object
since there is no equvalent ParentObject
mixin class here). As you would expect, this is also the default typeclass used for creating Characters if you don’t specify it. You can verify it:
> examine me
------------------------------------------------------------------------------
Name/key: YourName (#1)
Session id(s): #1
Account: YourName
Account Perms: <Superuser> (quelled)
Typeclass: Character (typeclasses.characters.Character)
Location: Limbo (#2)
Home: Limbo (#2)
Permissions: developer, player
Locks: boot:false(); call:false(); control:perm(Developer); delete:false();
drop:holds(); edit:false(); examine:perm(Developer); get:false();
msg:all(); puppet:false(); tell:perm(Admin); view:all()
Stored Cmdset(s):
commands.default_cmdsets.CharacterCmdSet [DefaultCharacter] (Union, prio 0)
Merged Cmdset(s):
...
Commands available to YourName (result of Merged CmdSets):
...
Persistent attributes:
desc = This is User #1.
prelogout_location = Limbo
Non-Persistent attributes:
last_cmd = None
------------------------------------------------------------------------------
Yes, the examine
command understands me
. You got a lot longer output this time. You have a lot more going on than a simple Object. Here are some new fields of note:
Session id(s): This identifies the Session (that is, the individual connection to a player’s game client).
Account shows, well the
Account
object associated with this Character and Session.Stored/Merged Cmdsets and Commands available is related to which Commands are stored on you. We will get to them in the next lesson. For now it’s enough to know these consitute all the commands available to you at a given moment.
Non-Persistent attributes are Attributes that are only stored temporarily and will go away on next reload.
Look at the Typeclass field and you’ll find that it points to typeclasses.character.Character
as expected. So if we modify this class we’ll also modify ourselves.
7.3.1. A method on ourselves¶
Let’s try something simple first. Back in mygame/typeclasses/characters.py
:
class Character(DefaultCharacter):
"""
(class docstring)
"""
strength = 10
dexterity = 12
intelligence = 15
def get_stats(self):
"""
Get the main stats of this character
"""
return self.strength, self.dexterity, self.intelligence
> reload
> py self.get_stats()
(10, 12, 15)
We made a new method, gave it a docstring and had it return
the RP-esque values we set. It comes back as a tuple (10, 12, 15)
. To get a specific value you could specify the index of the value you want, starting from zero:
> py stats = self.get_stats() ; print(f"Strength is {stats[0]}.")
Strength is 10.
7.3.2. Attributes¶
So what happens when we increase our strength? This would be one way:
> py self.strength = self.str + 1
> py self.strength
11
Here we set the strength equal to its previous value + 1. A shorter way to write this is to use Python’s +=
operator:
> py self.strength += 1
> py self.strength
12
> py self.get_stats()
(12, 12, 15)
This looks correct! Try to change the values for dex and int too; it works fine. However:
> reload
> py self.get_stats()
(10, 12, 15)
After a reload all our changes were forgotten. When we change properties like this, it only changes in memory, not in the database (nor do we modify the python module’s code). So when we reloaded, the ‘fresh’ Character
class was loaded, and it still has the original stats we wrote in it.
In principle we could change the python code. But we don’t want to do that manually every time. And more importantly since we have the stats hardcoded in the class, every character instance in the game will have exactly the same str
, dex
and int
now! This is clearly not what we want.
Evennia offers a special, persistent type of property for this, called an Attribute
. Rework your
mygame/typeclasses/characters.py
like this:
class Character(DefaultCharacter):
"""
(class docstring)
"""
def get_stats(self):
"""
Get the main stats of this character
"""
return self.db.strength, self.db.dexterity, self.db.intelligence
We removed the hard-coded stats and added added .db
for every stat. The .db
handler makes the stat into an an Evennia Attribute.
> reload
> py self.get_stats()
(None, None, None)
Since we removed the hard-coded values, Evennia don’t know what they should be (yet). So all we get back is None
, which is a Python reserved word to represent nothing, a no-value. This is different from a normal python property:
> py me.strength
AttributeError: 'Character' object has no attribute 'strength'
> py me.db.strength
(nothing will be displayed, because it's None)
Trying to get an unknown normal Python property will give an error. Getting an unknown Evennia Attribute
will never give an error, but only result in None
being returned. This is often very practical.
Next, let us test out assigning those Attributes
> py me.db.strength, me.db.dexterity, me.db.intelligence = 10, 12, 15
> py me.get_stats()
(10, 12, 15)
> reload
> py me.get_stats()
(10, 12, 15)
Now we set the Attributes to the right values, and they survive a server reload! Let’s modify the strength:
> py self.db.strength += 2
> py self.get_stats()
(12, 12, 15)
> reload
> py self.get_stats()
(12, 12, 15)
Also our change now survives a reload since Evennia automatically saves the Attribute to the database for us.
7.3.3. Setting things on new Characters¶
Things are looking better, but one thing remains strange - the stats start out with a value None
and we have to manually set them to something reasonable. In a later lesson we will investigate character-creation in more detail. For now, let’s give every new character some random stats to start with.
We want those stats to be set only once, when the object is first created. For the Character, this method is called at_object_creation
.
# up by the other imports
import random
class Character(DefaultCharacter):
"""
(class docstring)
"""
def at_object_creation(self):
self.db.strength = random.randint(3, 18)
self.db.dexterity = random.randint(3, 18)
self.db.intelligence = random.randint(3, 18)
def get_stats(self):
"""
Get the main stats of this character
"""
return self.db.strength, self.db.dexterity, self.db.intelligence
We imported a new module, random
. This is part of Python’s standard library. We used random.randint
to
set a random value from 3 to 18 to each stat. Simple, but for some classical RPGs this is all you need!
> reload
> py self.get_stats()
(12, 12, 15)
Hm, this is the same values we set before. They are not random. The reason for this is of course that, as said, at_object_creation
only runs once, the very first time a character is created. Our character object was already created long before, so it will not be called again.
It’s simple enough to run it manually though:
> self.at_object_creation()
> py self.get_stats()
(5, 4, 8)
Lady luck didn’t smile on us for this example; maybe you’ll fare better. Evennia has a helper command
update
that re-runs the creation hook and also cleans up any other Attributes not re-created by at_object_creation
:
> update self
> py self.get_stats()
(8, 16, 14)
7.3.4. Updating all Characters in a loop¶
Needless to say, you are wise to have a feel for what you want to go into the at_object_creation
hook before you create a lot of objects (characters in this case).
Luckily you only need to update objects once, and you don’t have to go around and re-run the at_object_creation
method on everyone manually. For this we’ll try out a Python loop. Let’s go into multi-line Python mode:
> py
> for a in [1, 2, "foo"]:
> print(a)
1
2
foo
A python for-loop allows us to loop over something. Above, we made a list of two numbers and a string. In every iteration of the loop, the variable a
becomes one element in turn, and we print that.
For our list, we want to loop over all Characters, and want to call .at_object_creation
on each. This is how this is done (still in python multi-line mode):
> from typeclasses.characters import Character
> for char in Character.objects.all()
> char.at_object_creation()
We import the Character
class and then we use .objects.all()
to get all Character
instances. Simplified,
.objects
is a resource from which one can query for all Characters
. Using .all()
gets us a listing
of all of them that we then immediately loop over. Boom, we just updated all Characters, including ourselves:
> quit()
Closing the Python console.
> self.get_stats()
(3, 18, 10)
7.4. Extra Credits¶
This principle is the same for other typeclasses. So using the tools explored in this lesson, try to expand the default room with an is_dark
flag. It can be either True
or False
. Have all new rooms start with is_dark = False
and make it so that once you change it, it survives a reload.
Oh, and if you created any other rooms before, make sure they get the new flag too!
7.5. Conclusions¶
In this lesson we created database-persistent dragons by having their classes inherit from one Object
, one of Evennia’s typeclasses. We explored where Evennia looks for typeclasses if we don’t specify the path explicitly. We then modified ourselves - via the Character
class - to give us some simple RPG stats. This led to the need to use Evennia’s Attributes, settable via .db
and to use a for-loop to update ourselves.
Typeclasses are a fundamental part of Evennia and we will see a lot of more uses of them in the course of this tutorial. But that’s enough of them for now. It’s time to take some action. Let’s learn about Commands.