diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ffa74b003..f9f0ea7e3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ - New `utils.format_grid` for easily displaying long lists of items in a block. - Using `lunr` search indexing for better `help` matching and suggestions. Also improve the main help command's default listing output. - +- Added `content_types` indexing to DefaultObject's ContentsHandler. (volund) ### Already in master - `is_typeclass(obj (Object), exact (bool))` now defaults to exact=False diff --git a/evennia/objects/models.py b/evennia/objects/models.py index f9e7799d48..8b42832687 100644 --- a/evennia/objects/models.py +++ b/evennia/objects/models.py @@ -13,6 +13,7 @@ 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 collections import defaultdict from django.conf import settings from django.db import models from django.core.exceptions import ObjectDoesNotExist @@ -42,34 +43,49 @@ class ContentsHandler(object): """ self.obj = obj - self._pkcache = {} + self._pkcache = set() self._idcache = obj.__class__.__instance_cache__ + self._typecache = defaultdict(set) self.init() + def load(self): + """ + Retrieves all objects from database. Used for initializing. + + Returns: + Objects (list of ObjectDB) + """ + return list(self.obj.locations_set.all()) + def init(self): """ Re-initialize the content cache """ - self._pkcache.update( - dict((obj.pk, None) for obj in ObjectDB.objects.filter(db_location=self.obj) if obj.pk) - ) + objects = self.load() + self._pkcache = {obj.pk for obj in objects} + for obj in objects: + for ctype in obj._content_types: + self._typecache[ctype].add(obj.pk) - def get(self, exclude=None): + def get(self, exclude=None, content_type=None): """ Return the contents of the cache. Args: exclude (Object or list of Object): object(s) to ignore + content_type (str or None): Filter list by a content-type. If None, don't filter. Returns: objects (list): the Objects inside this location """ - if exclude: - pks = [pk for pk in self._pkcache if pk not in [excl.pk for excl in make_iter(exclude)]] + if content_type is not None: + pks = self._typecache[content_type] else: pks = self._pkcache + if exclude: + pks = pks - {excl.pk for excl in make_iter(exclude)} try: return [self._idcache[pk] for pk in pks] except KeyError: @@ -81,7 +97,7 @@ class ContentsHandler(object): except KeyError: # this means an actual failure of caching. Return real database match. logger.log_err("contents cache failed for %s." % self.obj.key) - return list(ObjectDB.objects.filter(db_location=self.obj)) + return self.load() def add(self, obj): """ @@ -91,7 +107,9 @@ class ContentsHandler(object): obj (Object): object to add """ - self._pkcache[obj.pk] = None + self._pkcache.add(obj.pk) + for ctype in obj._content_types: + self._typecache[ctype].add(obj.pk) def remove(self, obj): """ @@ -101,7 +119,10 @@ class ContentsHandler(object): obj (Object): object to remove """ - self._pkcache.pop(obj.pk, None) + self._pkcache.remove(obj.pk) + for ctype in obj._content_types: + if obj.pk in self._typecache[ctype]: + self._typecache[ctype].remove(obj.pk) def clear(self): """ @@ -109,6 +130,7 @@ class ContentsHandler(object): """ self._pkcache = {} + self._typecache = defaultdict(set) self.init() diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 98922ddbf9..3081a2658b 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -203,6 +203,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): without `obj.save()` having to be called explicitly. """ + # Used for sorting / filtering in inventories / room contents. + _content_types = ("object",) # lockstring of newly created objects, for easy overloading. # Will be formatted with the appropriate attributes. @@ -257,7 +259,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): and not self.db_account.attributes.get("_quell") ) - def contents_get(self, exclude=None): + def contents_get(self, exclude=None, content_type=None): """ Returns the contents of this object, i.e. all objects that has this object set as its location. @@ -266,17 +268,18 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): Args: exclude (Object): Object to exclude from returned contents list + content_type (str): A content_type to filter by. None for no + filtering. Returns: contents (list): List of contents of this Object. Notes: - Also available as the `contents` property. + Also available as the `contents` property, minus exclusion + and filtering. """ - con = self.contents_cache.get(exclude=exclude) - # print "contents_get:", self, con, id(self), calledby() # DEBUG - return con + return self.contents_cache.get(exclude=exclude, content_type=content_type) def contents_set(self, *args): "You cannot replace this property" @@ -1656,20 +1659,25 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): **kwargs (dict): Arbitrary, optional arguments for users overriding the call (unused by default). """ + def filter_visible(obj_list): + # Helper method to determine if objects are visible to the looker. + return [obj for obj in obj_list if obj != looker and obj.access(looker, "view")] + if not looker: return "" + # get and identify all objects - visible = (con for con in self.contents if con != looker and con.access(looker, "view")) - exits, users, things = [], [], defaultdict(list) - for con in visible: - key = con.get_display_name(looker) - if con.destination: - exits.append(key) - elif con.has_account: - users.append("|c%s|n" % key) - else: - # things can be pluralized - things[key].append(con) + exits_list = filter_visible(self.contents_get(content_type='exit')) + users_list = filter_visible(self.contents_get(content_type='character')) + things_list = filter_visible(self.contents_get(content_type="object")) + + things = defaultdict(list) + + for thing in things_list: + things[thing.key].append(thing) + users = [f"|c{user.key}|n" for user in users_list] + exits = [ex.key for ex in exits_list] + # get description, build string string = "|c%s|n\n" % self.get_display_name(looker) desc = self.db.desc @@ -2026,7 +2034,9 @@ class DefaultCharacter(DefaultObject): a character avatar controlled by an account. """ - + # Tuple of types used for indexing inventory contents. Characters generally wouldn't be in + # anyone's inventory, but this also governs displays in room contents. + _content_types = ("character",) # lockstring of newly created rooms, for easy overloading. # Will be formatted with the appropriate attributes. lockstring = "puppet:id({character_id}) or pid({account_id}) or perm(Developer) or pperm(Developer);delete:id({account_id}) or perm(Admin)" @@ -2278,6 +2288,9 @@ class DefaultRoom(DefaultObject): This is the base room object. It's just like any Object except its location is always `None`. """ + # A tuple of strings used for indexing this object inside an inventory. + # Generally, a room isn't expected to HAVE a location, but maybe in some games? + _content_types = ("room",) # lockstring of newly created rooms, for easy overloading. # Will be formatted with the {id} of the creating object. @@ -2428,7 +2441,7 @@ class DefaultExit(DefaultObject): exits simply by giving the exit-object's name on its own. """ - + _content_types = ("exit",) exit_command = ExitCommand priority = 101