evennia/evennia/objects/models.py

299 lines
11 KiB
Python

"""
This module defines the database models for all in-game objects, that
is, all objects that has an actual existence in-game.
Each database object is 'decorated' with a 'typeclass', a normal
python class that implements all the various logics needed by the game
in question. Objects created of this class transparently communicate
with its related database object for storing all attributes. The
admin should usually not have to deal directly with this database
object layer.
Attributes are separate objects that store values persistently onto
the database object. Like everything else, they can be accessed
transparently through the decorating TypeClass.
"""
from django.conf import settings
from django.db import models
from django.core.exceptions import ObjectDoesNotExist
from evennia.typeclasses.models import TypedObject
from evennia.objects.manager import ObjectDBManager
from evennia.utils import logger
from evennia.utils.utils import (make_iter, dbref, lazy_property)
class ContentsHandler(object):
"""
Handles and caches the contents of an object
to avoid excessive lookups (this is done very
often due to cmdhandler needing to look for
object-cmdsets). It is stored on the 'contents_cache'
property of the ObjectDB.
"""
def __init__(self, obj):
"""
Sets up the contents handler.
Args:
obj (Object): The object on which the
handler is defined
"""
self.obj = obj
self._cache = {}
self.init()
def init(self):
"""
Re-initialize the content cache
"""
self._cache.update(dict((obj.pk, obj) for obj in
ObjectDB.objects.filter(db_location=self.obj)))
def get(self, exclude=None):
"""
Return the contents of the cache.
Args:
exclude (Object or list of Object): object(s) to ignore
Returns:
objects (list): the Objects inside this location
"""
if exclude:
exclude = [excl.pk for excl in make_iter(exclude)]
return [obj for key, obj in self._cache.items() if key not in exclude]
return self._cache.values()
def add(self, obj):
"""
Add a new object to this location
Args:
obj (Object): object to add
"""
self._cache[obj.pk] = obj
def remove(self, obj):
"""
Remove object from this location
Args:
obj (Object): object to remove
"""
self._cache.pop(obj.pk, None)
def clear(self):
"""
Clear the contents cache and re-initialize
"""
self._cache = {}
self._init()
#------------------------------------------------------------
#
# ObjectDB
#
#------------------------------------------------------------
class ObjectDB(TypedObject):
"""
All objects in the game use the ObjectDB model to store
data in the database. This is handled transparently through
the typeclass system.
Note that the base objectdb is very simple, with
few defined fields. Use attributes to extend your
type class with new database-stored variables.
The TypedObject supplies the following (inherited) properties:
key - main name
name - alias for key
typeclass_path - the path to the decorating typeclass
typeclass - auto-linked typeclass
date_created - time stamp of object creation
permissions - perm strings
locks - lock definitions (handler)
dbref - #id of object
db - persistent attribute storage
ndb - non-persistent attribute storage
The ObjectDB adds the following properties:
player - optional connected player (always together with sessid)
sessid - optional connection session id (always together with player)
location - in-game location of object
home - safety location for object (handler)
scripts - scripts assigned to object (handler from typeclass)
cmdset - active cmdset on object (handler from typeclass)
aliases - aliases for this object (property)
nicks - nicknames for *other* things in Evennia (handler)
sessions - sessions connected to this object (see also player)
has_player - bool if an active player is currently connected
contents - other objects having this object as location
exits - exits from this object
"""
#
# ObjectDB Database model setup
#
#
# inherited fields (from TypedObject):
# db_key (also 'name' works), db_typeclass_path, db_date_created,
# db_permissions
#
# These databse fields (including the inherited ones) should normally be
# managed by their corresponding wrapper properties, named same as the
# field, but without the db_* prefix (e.g. the db_key field is set with
# self.key instead). The wrappers are created at the metaclass level and
# will automatically save and cache the data more efficiently.
# If this is a character object, the player is connected here.
db_player = models.ForeignKey("players.PlayerDB", null=True, verbose_name='player', on_delete=models.SET_NULL,
help_text='a Player connected to this object, if any.')
# the session id associated with this player, if any
db_sessid = models.CommaSeparatedIntegerField(null=True, max_length=32, verbose_name="session id",
help_text="csv list of session ids of connected Player, if any.")
# The location in the game world. Since this one is likely
# to change often, we set this with the 'location' property
# to transparently handle Typeclassing.
db_location = models.ForeignKey('self', related_name="locations_set", db_index=True, on_delete=models.SET_NULL,
blank=True, null=True, verbose_name='game location')
# a safety location, this usually don't change much.
db_home = models.ForeignKey('self', related_name="homes_set", on_delete=models.SET_NULL,
blank=True, null=True, verbose_name='home location')
# destination of this object - primarily used by exits.
db_destination = models.ForeignKey('self', related_name="destinations_set", db_index=True, on_delete=models.SET_NULL,
blank=True, null=True, verbose_name='destination',
help_text='a destination, used only by exit objects.')
# database storage of persistant cmdsets.
db_cmdset_storage = models.CharField('cmdset', max_length=255, null=True, blank=True,
help_text="optional python path to a cmdset class.")
# Database manager
objects = ObjectDBManager()
# defaults
__settingsclasspath__ = settings.BASE_OBJECT_TYPECLASS
__defaultclasspath__ = "evennia.objects.objects.DefaultObject"
__applabel__ = "objects"
@lazy_property
def contents_cache(self):
return ContentsHandler(self)
# cmdset_storage property handling
def __cmdset_storage_get(self):
"getter"
storage = self.db_cmdset_storage
return [path.strip() for path in storage.split(',')] if storage else []
def __cmdset_storage_set(self, value):
"setter"
self.db_cmdset_storage = ",".join(str(val).strip() for val in make_iter(value))
self.save(update_fields=["db_cmdset_storage"])
def __cmdset_storage_del(self):
"deleter"
self.db_cmdset_storage = None
self.save(update_fields=["db_cmdset_storage"])
cmdset_storage = property(__cmdset_storage_get, __cmdset_storage_set, __cmdset_storage_del)
# location getsetter
def __location_get(self):
"Get location"
return self.db_location
def __location_set(self, location):
"Set location, checking for loops and allowing dbref"
if isinstance(location, (basestring, int)):
# allow setting of #dbref
dbid = dbref(location, reqhash=False)
if dbid:
try:
location = ObjectDB.objects.get(id=dbid)
except ObjectDoesNotExist:
# maybe it is just a name that happens to look like a dbid
pass
try:
def is_loc_loop(loc, depth=0):
"Recursively traverse target location, trying to catch a loop."
if depth > 10:
return
elif loc == self:
raise RuntimeError
elif loc == None:
raise RuntimeWarning
return is_loc_loop(loc.db_location, depth + 1)
try:
is_loc_loop(location)
except RuntimeWarning:
pass
# if we get to this point we are ready to change location
old_location = self.db_location
# this is checked in _db_db_location_post_save below
self._safe_contents_update = True
# actually set the field (this will error if location is invalid)
self.db_location = location
self.save(update_fields=["db_location"])
# remove the safe flag
del self._safe_contents_update
# update the contents cache
if old_location:
old_location.contents_cache.remove(self)
if self.db_location:
self.db_location.contents_cache.add(self)
except RuntimeError:
errmsg = "Error: %s.location = %s creates a location loop." % (self.key, location)
logger.log_errmsg(errmsg)
raise #RuntimeError(errmsg)
except Exception, e:
errmsg = "Error (%s): %s is not a valid location." % (str(e), location)
logger.log_errmsg(errmsg)
raise #Exception(errmsg)
def __location_del(self):
"Cleanly delete the location reference"
self.db_location = None
self.save(update_fields=["db_location"])
location = property(__location_get, __location_set, __location_del)
def at_db_location_postsave(self, new):
"""
This is called automatically after the location field was
saved, no matter how. It checks for a variable
_safe_contents_update to know if the save was triggered via
the location handler (which updates the contents cache) or
not.
"""
if not hasattr(self, "_safe_contents_update"):
# changed/set outside of the location handler
if new:
# if new, there is no previous location to worry about
if self.db_location:
self.db_location.contents_cache.add(self)
else:
# Since we cannot know at this point was old_location was, we
# trigger a full-on contents_cache update here.
logger.log_warn("db_location direct save triggered contents_cache.init() for all objects!")
[o.contents_cache.init() for o in self.__dbclass__.get_all_cached_instances()]
class Meta:
"Define Django meta options"
verbose_name = "Object"
verbose_name_plural = "Objects"