mirror of
https://github.com/evennia/evennia.git
synced 2026-03-18 13:56:30 +01:00
378 lines
16 KiB
ReStructuredText
378 lines
16 KiB
ReStructuredText
Attributes
|
|
==========
|
|
|
|
When performing actions in Evennia it is often important that you store
|
|
data for later. If you write a menu system, you have to keep track of
|
|
the current location in the menu tree so that the player can give
|
|
correct subsequent commands. If you are writing a combat system, you
|
|
might have a combattant's next roll get easier dependent on if their
|
|
opponent failed. Your characters will probably need to store
|
|
roleplaying-attributes like strength and agility. And so on.
|
|
|
|
`Typeclassed <Typeclasses.html>`_ game entities
|
|
(`Players <Players.html>`_, `Objects <Objects.html>`_ and
|
|
`Scripts <Scripts.html>`_) always have *Attributes* associated with
|
|
them. Attributes are used to store any type of data 'on' such entities.
|
|
This is different from storing data in properties already defined on
|
|
entities (such as ``key`` or ``location``) - these have very specific
|
|
names and require very specific types of data (for example you couldn't
|
|
assign a python *list* to the ``key`` property no matter how hard you
|
|
tried). ``Attributes`` come into play when you want to assign arbitrary
|
|
data to arbitrary names.
|
|
|
|
Saving and Retrieving data
|
|
--------------------------
|
|
|
|
To save persistent data on a Typeclassed object you normally use the
|
|
``db`` (DataBase) operator. Let's try to save some data to a *Rose* (an
|
|
`Object <Objects.html>`_):
|
|
|
|
::
|
|
|
|
# saving
|
|
rose.db.has_thorns = True
|
|
# getting it back
|
|
is_ouch = rose.db.has_thorns
|
|
|
|
This looks like any normal Python assignment, but that ``db`` makes sure
|
|
that an *Attribute* is created behind the scenes and is stored in the
|
|
database. Your rose will continue to have thorns throughout the life of
|
|
the server now, until you deliberately remove them.
|
|
|
|
To be sure to save **non-persistently**, i.e. to make sure NOT to create
|
|
a database entry, you use ``ndb`` (NonDataBase). It works in the same
|
|
way:
|
|
|
|
::
|
|
|
|
# saving
|
|
rose.ndb.has_thorns = True
|
|
# getting it back
|
|
is_ouch = rose.ndb.has_thorns
|
|
|
|
Strictly speaking, ``ndb`` has nothing to do with ``Attributes``,
|
|
despite how similar they look. No ``Attribute`` object is created behind
|
|
the scenes when using ``ndb``. In fact the database is not invoked at
|
|
all since we are not interested in persistence.
|
|
|
|
You can also ``del`` properties on ``db`` and ``ndb`` as normal. This
|
|
will for example delete an ``Attribute``:
|
|
|
|
::
|
|
|
|
del rose.db.has_thorns
|
|
|
|
Both ``db`` and ``ndb`` defaults to offering an ``all`` property on
|
|
themselves. This returns all associated attributes or non-persistent
|
|
properties.
|
|
|
|
::
|
|
|
|
list_of_all_rose_attributes = rose.db.all
|
|
list_of_all_rose_ndb_attrs = rose.ndb.all
|
|
|
|
If you use ``all`` as the name of an attribute, this will be used
|
|
instead. Later deleting your custom ``all`` will return the default
|
|
behaviour.
|
|
|
|
Fast assignment
|
|
---------------
|
|
|
|
*Depracation Warning: Fast assigment is deprecated and should not be
|
|
used - it will be removed in the future. Use the ``db`` operator
|
|
explicitly when saving to the database.*
|
|
|
|
For quick testing you can in principle skip the ``db`` operator and
|
|
assign Attributes like you would any normal Python property:
|
|
|
|
::
|
|
|
|
# saving
|
|
rose.has_thorns = True
|
|
# getting it back
|
|
is_ouch = rose.has_thorns
|
|
|
|
This looks like any normal Python assignment, but calls ``db`` behind
|
|
the scenes for you.
|
|
|
|
Note however that this form stands the chance of overloading already
|
|
existing properties on typeclasses and their database objects. Unless
|
|
you know what you are doing, this can cause lots of trouble.
|
|
|
|
::
|
|
|
|
rose.msg("hello") # this uses the in-built msg() method
|
|
rose.msg = "Ouch!" # this OVERLOADS the msg() method with a string
|
|
rose.msg("hello") # this now a gives traceback!
|
|
|
|
Overloading ``msg()`` with a string is a very bad idea since Evennia
|
|
uses this method all the time to send text to you. There are of course
|
|
situations when you *want* to overload default methods with your own
|
|
implementations - but then you'll hopefully do so intentionally and with
|
|
something that works.
|
|
|
|
::
|
|
|
|
rose.db.msg = "Ouch" # this stands no risk of overloading msg()
|
|
rose.msg("hello") # this works as it should
|
|
|
|
So using ``db``/``ndb`` will always do what you expect and is usually
|
|
the safer bet. It also makes it visually clear at all times when you are
|
|
saving to the database and not.
|
|
|
|
Another drawback of this shorter form is that it will handle a non-found
|
|
Attribute as it would any non-found property on the object. The ``db``
|
|
operator will instead return ``None`` if no matching Attribute is found.
|
|
So if an object has no attribute (or property) named ``test``, doing
|
|
``obj.test`` will raise an ``AttributeException`` error, whereas
|
|
``obj.db.test`` will return ``None``.
|
|
|
|
Persistent vs non-persistent
|
|
----------------------------
|
|
|
|
So *persistent* data means that your data will survive a server reboot,
|
|
whereas with *non-persistent* data it will not ...
|
|
|
|
... So why would you ever want to use non-persistent data? The answer
|
|
is, you don't have to. Most of the time you really want to save as much
|
|
as you possibly can. Non-persistent data is potentially useful in a few
|
|
situations though.
|
|
|
|
- You are worried about database performance. Since Evennia caches
|
|
Attributes very aggressively, this is not an issue unless you are
|
|
reading *and* writing to your Attribute very often (like many times
|
|
per second). Reading from an already cached Attribute is as fast as
|
|
reading any Python property. But even then this is not likely
|
|
something to worry about: Apart from Evennia's own caching, modern
|
|
database systems themselves also cache data very efficiently for
|
|
speed. Our default database even runs completely in RAM if possible,
|
|
alleviating much of the need to write to disk during heavy loads.
|
|
- A more valid reason for using non-persistent data is if you *want* to
|
|
loose your state when logging off. Maybe you are storing throw-away
|
|
data that are re-initialized at server startup. Maybe you are
|
|
implementing some caching of your own. Or maybe you are testing a
|
|
buggy `Script <Scripts.html>`_ that does potentially harmful stuff to
|
|
your character object. With non-persistent storage you can be sure
|
|
that whatever is messed up, it's nothing a server reboot can't clear
|
|
up.
|
|
- You want to implement a fully or partly *non-persistent world*. Who
|
|
are we to argue with your grand vision!
|
|
|
|
What types of data can I save in an Attribute?
|
|
----------------------------------------------
|
|
|
|
Evennia uses the ``pickle`` module to serialize Attribute data into the
|
|
database. So if you store a single object (that is, not an iterable list
|
|
of objects), you can practically store any Python object that can be
|
|
`pickled <http://docs.python.org/library/pickle.html>`_.
|
|
|
|
If you store many objects however, you can only store them using normal
|
|
Python structures (i.e. in either a *tuple*, *list*, *dictionary* or
|
|
*set*). All other iterables (such as custom containers) are converted to
|
|
*lists* by the Attribute (see next section for the reason for this).
|
|
Since you can nest dictionaries, sets, lists and tuples together in any
|
|
combination, this is usually not much of a limitation.
|
|
|
|
There is one notable type of object that cannot be pickled - and that is
|
|
a Django database object. These will instead be stored as a wrapper
|
|
object containing the ID and its database model. It will be read back to
|
|
a new instantiated `typeclass <Typeclasses.html>`_ when the Attribute is
|
|
accessed. Since erroneously trying to save database objects in an
|
|
Attribute will lead to errors, Evennia will try to detect database
|
|
objects by analyzing the data being stored. This means that Evennia must
|
|
recursively traverse all iterables to make sure all database objects in
|
|
them are stored safely. So for efficiency, it can be a good idea to
|
|
avoid deeply nested lists with objects if you can.
|
|
|
|
*Note that you could fool the safety check if you for example created
|
|
custom, non-iterable classes and stored database objects in them. So to
|
|
make this clear - saving such an object is **not supported** and will
|
|
probably make your game unstable. Store your database objects using
|
|
lists, tuples, dictionaries, sets or a combination of the four and you
|
|
should be fine.*
|
|
|
|
Examples of valid attribute data:
|
|
|
|
::
|
|
|
|
# a single value
|
|
obj.db.test1 = 23
|
|
obj.db.test1 = False
|
|
# a database object (will be stored as dbref)
|
|
obj.db.test2 = myobj
|
|
# a list of objects
|
|
obj.db.test3 = [obj1, 45, obj2, 67]
|
|
# a dictionary
|
|
obj.db.test4 = {'str':34, 'dex':56, 'agi':22, 'int':77}
|
|
# a mixed dictionary/list
|
|
obj.db.test5 = {'members': [obj1,obj2,obj3], 'enemies':[obj4,obj5]}
|
|
# a tuple with a list in it
|
|
obj.db.test6 = (1,3,4,8, ["test", "test2"], 9)
|
|
# a set will still be stored and returned as a list [1,2,3,4,5]!
|
|
obj.db.test7 = set([1,2,3,4,5])
|
|
# in-situ manipulation
|
|
obj.db.test8 = [1,2,{"test":1}]
|
|
obj.db.test8[0] = 4
|
|
obj.db.test8[2]["test"] = 5
|
|
# test8 is now [4,2,{"test":5}]
|
|
|
|
Example of non-supported save:
|
|
|
|
::
|
|
|
|
# this will fool the dbobj-check since myobj (a database object) is "hidden"
|
|
# inside a custom object. This is unsupported and will lead to unexpected
|
|
# results!
|
|
class BadStorage(object):
|
|
pass
|
|
bad = BadStorage()
|
|
bad.dbobj = myobj
|
|
obj.db.test8 = bad # this will likely lead to a traceback
|
|
|
|
Retrieving Mutable objects
|
|
--------------------------
|
|
|
|
A side effect of the way Evennia stores Attributes is that Python Lists,
|
|
Dictionaries and Sets are handled by custom objects called PackedLists,
|
|
PackedDicts and PackedSets. These behave just like normal lists and
|
|
dicts except they have the special property that they save to the
|
|
database whenever new data gets assigned to them. This allows you to do
|
|
things like ``self.db.mylist[4]`` = val without having to extract the
|
|
mylist Attribute into a temporary variable first.
|
|
|
|
There is however an important thing to remember. If you retrieve this
|
|
data into another variable, e.g. ``mylist2 = obj.db.mylist``, your new
|
|
variable (``mylist2``) will *still* be a PackedList! This means it will
|
|
continue to save itself to the database whenever it is updated! This is
|
|
important to keep in mind so you are not confused by the results.
|
|
|
|
::
|
|
|
|
obj.db.mylist = [1,2,3,4]
|
|
mylist = obj.db.mylist
|
|
mylist[3] = 5 # this will also update database
|
|
print mylist # this is now [1,2,3,5]
|
|
print mylist.db.mylist # this is also [1,2,3,5]
|
|
|
|
To "disconnect" your extracted mutable variable from the database you
|
|
simply need to convert the PackedList or PackedDict to a normal Python
|
|
list or dictionary. This is done with the builtin ``list()`` and
|
|
``dict()`` functions. In the case of "nested" lists and dicts, you only
|
|
have to convert the "outermost" list/dict in order to cut the entire
|
|
structure's connection to the database.
|
|
|
|
::
|
|
|
|
obj.db.mylist = [1,2,3,4]
|
|
mylist = list(obj.db.mylist) # convert to normal list
|
|
mylist[3] = 5
|
|
print mylist # this is now [1,2,3,5]
|
|
print obj.db.mylist # this remains [1,2,3,4]
|
|
|
|
Remember, this is only valid for mutable iterables - lists and dicts and
|
|
combinations of the two.
|
|
`Immutable <http://en.wikipedia.org/wiki/Immutable>`_ objects (strings,
|
|
numbers, tuples etc) are already disconnected from the database from the
|
|
onset. So making the outermost iterable into a tuple is also a way to
|
|
stop any changes to the structure from updating the database.
|
|
|
|
::
|
|
|
|
obj.db.mytup = (1,2,[3,4])
|
|
obj.db.mytup[0] = 5 # this fails since tuples are immutable
|
|
obj.db.mytup[2][1] = 5 # this works but will NOT update database since outermost iterable is a tuple
|
|
print obj.db.mytup[2][1] # this still returns 4, not 5
|
|
mytup1 = obj.db.mytup
|
|
# mytup1 is already disconnected from database since outermost
|
|
# iterable is a tuple, so we can edit the internal list as we want
|
|
# without affecting the database.
|
|
|
|
Locking and checking Attributes
|
|
-------------------------------
|
|
|
|
Attributes are normally not locked down by default, but you can easily
|
|
change that for individual Attributes (like those that may be
|
|
game-sensitive in games with user-level building).
|
|
|
|
First you need to set a *lock string* on your Attribute. Lock strings
|
|
are specified `here <Locks.html>`_. The relevant lock types are
|
|
|
|
- *attrread* - limits who may read the value of the Attribute
|
|
- *attredit* - limits who may set/change this Attribute
|
|
|
|
You cannot use e.g. ``obj.db.attrname`` handler to modify Attribute
|
|
objects (such as setting a lock on them - you will only get the
|
|
Attribute *value* that way, not the actual Attribute *object*. You get
|
|
the latter with ``get_attribute_obj`` (see next section) which allows
|
|
you to set the lock something like this:
|
|
|
|
::
|
|
|
|
obj.get_attribute_obj.locks.add("attread:all();attredit:perm(Wizards)")
|
|
|
|
A lock is no good if nothing checks it -- and by default Evennia does
|
|
not check locks on Attributes. You have to add a check to your
|
|
commands/code wherever it fits (such as before setting an Attribute).
|
|
|
|
::
|
|
|
|
# in some command code where we want to limit
|
|
# setting of a given attribute name on an object
|
|
attr = obj.get_attribute_obj(attrname, default=None)
|
|
if not (attr and attr.locks.check(caller, 'attredit', default=True)):
|
|
caller.msg("You cannot edit that Attribute!")
|
|
return
|
|
# edit the Attribute here
|
|
|
|
Note that in this example this lock check will default to ``True`` if no
|
|
lock was defined on the Attribute (which is the case by default). You
|
|
can set this to False if you know all your Attributes always check
|
|
access in all situations. If you want some special control over what the
|
|
default Attribute access is (such as allowing everyone to view, but
|
|
never allowing anyone to edit unless explicitly allowing it with a
|
|
lock), you can use the ``secure_attr`` method on Typeclassed objects
|
|
like this:
|
|
|
|
::
|
|
|
|
obj.secure_attr(caller, attrname, value=None,
|
|
delete=False,
|
|
default_access_read=True,
|
|
default_access_edit=False,
|
|
default_access_create=True)
|
|
|
|
The secure\_attr will try to retrieve the attribute value of an existing
|
|
Attribute if the ``value`` keyword is not set and create/set/delete it
|
|
otherwise. The *default\_access* keywords specify what should be the
|
|
default policy for each operation if no appropriate lock string is set
|
|
on the Attribute.
|
|
|
|
Other ways to access Attributes
|
|
-------------------------------
|
|
|
|
Normally ``db`` is all you need. But there there are also several other
|
|
ways to access information about Attributes, some of which cannot be
|
|
replicated by ``db``. These are available on all Typeclassed objects:
|
|
|
|
- ``has_attribute(attrname)`` - checks if the object has an attribute
|
|
with the given name. This is equivalent to doing ``obj.db.attrname``.
|
|
- ``set_attribute(attrname, value)`` - equivalent to
|
|
``obj.db.attrname = value``.
|
|
- ``get_attribute(attrname)`` - returns the attribute value. Equivalent
|
|
to ``obj.db.attrname``.
|
|
- ``get_attribute_raise(attrname)`` - returns the attribute value, but
|
|
instead of returning ``None`` if no such attribute is found, this
|
|
method raises ``AttributeError``.
|
|
- ``get_attribute_obj(attrname)`` - returns the attribute *object*
|
|
itself rather than the value stored in it.
|
|
- ``del_attribute(attrname)`` - equivalent to ``del obj.db.attrname``.
|
|
Quietly fails if ``attrname`` is not found.
|
|
- ``del_attribute_raise(attrname)`` - deletes attribute, raising
|
|
``AttributeError`` if no matching Attribute is found.
|
|
- ``get_all_attributes`` - equivalent to ``obj.db.all``
|
|
- ``attr(attrname, value=None, delete=False)`` - this is a convenience
|
|
function for getting, setting and deleting Attributes. It's
|
|
recommended to use ``db`` instead.
|
|
- ``secure_attr(...)`` - lock-checking version of ``attr``. See example
|
|
in previous section.
|
|
|