diff --git a/evennia/commands/cmdhandler.py b/evennia/commands/cmdhandler.py index 0f13774780..2e64682392 100644 --- a/evennia/commands/cmdhandler.py +++ b/evennia/commands/cmdhandler.py @@ -171,7 +171,7 @@ def get_and_merge_cmdsets(caller, session, player, obj, # Gather all cmdsets stored on objects in the room and # also in the caller's inventory and the location itself local_objlist = yield (location.contents_get(exclude=obj) + - obj.contents + [location]) + obj.contents_get() + [location]) local_objlist = [o for o in local_objlist if not o._is_deleted] for lobj in local_objlist: try: diff --git a/evennia/objects/manager.py b/evennia/objects/manager.py index cf723d7fc6..753f485fd9 100644 --- a/evennia/objects/manager.py +++ b/evennia/objects/manager.py @@ -171,8 +171,7 @@ class ObjectDBManager(TypedObjectManager): @returns_typeclass_list def get_contents(self, location, excludeobj=None): """ - Get all objects that has a location - set to this one. + Get all objects that has a location set to this one. excludeobj - one or more object keys to exclude from the match """ diff --git a/evennia/objects/models.py b/evennia/objects/models.py index b532226647..ef5edaaf8f 100644 --- a/evennia/objects/models.py +++ b/evennia/objects/models.py @@ -20,9 +20,82 @@ 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) +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 @@ -105,6 +178,10 @@ class ObjectDB(TypedObject): # Database manager objects = ObjectDBManager() + @lazy_property + def contents_cache(self): + return ContentsHandler(self) + # cmdset_storage property handling def __cmdset_storage_get(self): "getter" @@ -152,9 +229,27 @@ class ObjectDB(TypedObject): is_loc_loop(location) except RuntimeWarning: pass - # actually set the field + + # 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) @@ -170,6 +265,20 @@ class ObjectDB(TypedObject): self.save(update_fields=["db_location"]) location = property(__location_get, __location_set, __location_del) + def _db_location_post_save(self): + """ + 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 proper handler or not. + + Since we cannot know at this point was old_location was, we + trigger a full-on contents_cache update here. + + """ + if not hasattr(self, "_safe_contents_update"): + 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" diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 6270ac250a..6fb7e2a527 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -294,7 +294,7 @@ class DefaultObject(ObjectDB): exclude is one or more objects to not return """ - return ObjectDB.objects.get_contents(self, excludeobj=exclude) + return self.contents_cache.get(exclude=exclude) contents = property(contents_get) @@ -709,7 +709,6 @@ class DefaultObject(ObjectDB): location or to default home. """ # Gather up everything that thinks this is its location. - objs = ObjectDB.objects.filter(db_location=self) default_home_id = int(settings.DEFAULT_HOME.lstrip("#")) try: default_home = ObjectDB.objects.get(id=default_home_id) @@ -721,7 +720,7 @@ class DefaultObject(ObjectDB): log_errmsg(string % default_home_id) default_home = None - for obj in objs: + for obj in self.contents: home = obj.home # Obviously, we can't send it back to here. if not home or (home and home.dbid == self.dbid): @@ -824,6 +823,7 @@ class DefaultObject(ObjectDB): self.attributes.clear() self.nicks.clear() self.aliases.clear() + self.location = None # this updates contents_cache for our location # Perform the deletion of the object super(ObjectDB, self).delete() diff --git a/evennia/server/portal/portalsessionhandler.py b/evennia/server/portal/portalsessionhandler.py index 5c600c79c9..f686fe1d67 100644 --- a/evennia/server/portal/portalsessionhandler.py +++ b/evennia/server/portal/portalsessionhandler.py @@ -73,7 +73,7 @@ class PortalSessionHandler(SessionHandler): if not self.portal.amp_protocol: # if amp is not yet ready (usually because the server is # booting up), try again a little later - reactor.CallLater(0.5, self.connect, session) + reactor.callLater(0.5, self.connect, session) return # sync with server-side diff --git a/evennia/server/profiling/dummyrunner_settings.py b/evennia/server/profiling/dummyrunner_settings.py index 2a0cfe98f8..8f2dd013e7 100644 --- a/evennia/server/profiling/dummyrunner_settings.py +++ b/evennia/server/profiling/dummyrunner_settings.py @@ -63,7 +63,7 @@ CHANCE_OF_ACTION = 0.5 # Chance of a currently unlogged-in dummy performing its login # action every tick. This emulates not all players logging in # at exactly the same time. -CHANCE_OF_LOGIN = 1.0#0.5 +CHANCE_OF_LOGIN = 1.0 # Which telnet port to connect to. If set to None, uses the first # default telnet port of the running server.