diff --git a/evennia/contrib/rpsystem.py b/evennia/contrib/rpsystem.py index 5443011461..3e4ae484b7 100644 --- a/evennia/contrib/rpsystem.py +++ b/evennia/contrib/rpsystem.py @@ -74,11 +74,13 @@ from builtins import object import re from re import escape as re_escape import itertools -from evennia import DefaultObject, DefaultCharacter +from django.conf import settings +from evennia import DefaultObject, DefaultCharacter, ObjectDB from evennia import Command, CmdSet from evennia import ansi -from evennia.utils.utils import lazy_property +from evennia.utils.utils import lazy_property, make_iter, variable_from_module +_AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1)) #------------------------------------------------------------ # Emote parser #------------------------------------------------------------ @@ -1056,67 +1058,149 @@ class ContribRPObject(DefaultObject): self.db.pose = "" self.db.pose_default = "is here." - def search(self, searchdata, **kwargs): + def search(self, searchdata, + global_search=False, + use_nicks=True, + typeclass=None, + location=None, + attribute_name=None, + quiet=False, + exact=False, + candidates=None, + nofound_string=None, + multimatch_string=None, + use_dbref=None): """ - This version of search will pre-parse searchdata for eventual - matches against recogs and sdescs of candidates in the same - location. + Returns an Object matching a search string/condition, taking + sdescs into account. + + Perform a standard object search in the database, handling + multiple results and lack thereof gracefully. By default, only + objects in the current `location` of `self` or its inventory are searched for. Args: - searchdata (str): Search string. + searchdata (str or obj): Primary search criterion. Will be matched + against `object.key` (with `object.aliases` second) unless + the keyword attribute_name specifies otherwise. + **Special strings:** + - `#`: search by unique dbref. This is always + a global search. + - `me,self`: self-reference to this object + - `-` - can be used to differentiate + between multiple same-named matches + global_search (bool): Search all objects globally. This is overruled + by `location` keyword. + use_nicks (bool): Use nickname-replace (nicktype "object") on `searchdata`. + typeclass (str or Typeclass, or list of either): Limit search only + to `Objects` with this typeclass. May be a list of typeclasses + for a broader search. + location (Object or list): Specify a location or multiple locations + to search. Note that this is used to query the *contents* of a + location and will not match for the location itself - + if you want that, don't set this or use `candidates` to specify + exactly which objects should be searched. + attribute_name (str): Define which property to search. If set, no + key+alias search will be performed. This can be used + to search database fields (db_ will be automatically + appended), and if that fails, it will try to return + objects having Attributes with this name and value + equal to searchdata. A special use is to search for + "key" here if you want to do a key-search without + including aliases. + quiet (bool): don't display default error messages - this tells the + search method that the user wants to handle all errors + themselves. It also changes the return value type, see + below. + exact (bool): if unset (default) - prefers to match to beginning of + string rather than not matching at all. If set, requires + exact mathing of entire string. + candidates (list of objects): this is an optional custom list of objects + to search (filter) between. It is ignored if `global_search` + is given. If not set, this list will automatically be defined + to include the location, the contents of location and the + caller's contents (inventory). + nofound_string (str): optional custom string for not-found error message. + multimatch_string (str): optional custom string for multimatch error header. + use_dbref (bool or None): If None, only turn off use_dbref if we are of a lower + permission than Builders. Otherwise, honor the True/False value. + + Returns: + match (Object, None or list): will return an Object/None if `quiet=False`, + otherwise it will return a list of 0, 1 or more matches. Notes: - Recog/sdesc matching is always turned off if the keyword - `global_search` is set or `candidates` are given. + To find Players, use eg. `evennia.player_search`. If + `quiet=False`, error messages will be handled by + `settings.SEARCH_AT_RESULT` and echoed automatically (on + error, return will be `None`). If `quiet=True`, the error + messaging is assumed to be handled by the caller. """ - if (isinstance(searchdata, basestring) and not - (kwargs.get("global_search") or - kwargs.get("candidates"))): - # searchdata is a string; common self-references + is_string = isinstance(searchdata, basestring) + + if is_string: + # searchdata is a string; wrap some common self-references if searchdata.lower() in ("here", ): - return [self.location] if "quiet" in kwargs else self.location + return [self.location] if quiet else self.location if searchdata.lower() in ("me", "self",): - return [self] if "quiet" in kwargs else self - if searchdata.lower() == self.key.lower(): - return [self] if "quiet" in kwargs else self + return [self] if quiet else self - # sdesc/recog matching - candidates = self.location.contents + self.contents - matches = parse_sdescs_and_recogs(self, candidates, - _PREFIX + searchdata, search_mode=True) - nmatches = len(matches) - if nmatches == 1: - return matches[0] - elif nmatches > 1: - # multimatch - reflist = ["%s%s%s (%s%s)" % (inum+1, _NUM_SEP, searchdata, self.recog.get(obj), - " (%s)" % self.key if self == obj else "") - for inum, obj in enumerate(matches)] - self.msg(_EMOTE_MULTIMATCH_ERROR.format(ref=searchdata,reflist="\n ".join(reflist))) - return - # No matches. At this point we can't pass this on to the - # normal search mechanism just like that, since that will lead to a - # security hole in the sdesc lookup: The normal search - # mechanism will search by key+alias, so that means if we - # were to guess a Character's key (or #dbref), their - # object would be returned regardless of their sdesc. So - # we limit the access to the parent search to builders. - # A side effect of this is that all objects searchable - # with this mechanism must be possible to search by sdesc. + if use_nicks: + # do nick-replacement on search + searchdata = self.nicks.nickreplace(searchdata, categories=("object", "player"), include_player=True) - if not self.locks.check_lockstring(self, "perm(Builders)"): - # we block lookup unless we have access to continue - if "nofound_string" in kwargs: - self.msg(kwargs["nofound_string"]) + if(global_search or (is_string and searchdata.startswith("#") and + len(searchdata) > 1 and searchdata[1:].isdigit())): + # only allow exact matching if searching the entire database + # or unique #dbrefs + exact = True + elif not candidates: + # no custom candidates given - get them automatically + if location: + # location(s) were given + candidates = [] + for obj in make_iter(location): + candidates.extend(obj.contents) + else: + # local search. Candidates are taken from + # self.contents, self.location and + # self.location.contents + location = self.location + candidates = self.contents + if location: + candidates = candidates + [location] + location.contents else: - self.msg("There is no '%s' here." % searchdata) - return + # normally we don't need this since we are + # included in location.contents + candidates.append(self) - # fall back to normal search - return super(ContribRPObject, self).search(searchdata, **kwargs) + # the sdesc-related substitution + if use_dbref is None: + use_dbref = self.locks.check_lockstring(self, "perm(Builders)") + if candidates: + candidates = parse_sdescs_and_recogs(self, candidates, + _PREFIX + searchdata, search_mode=True) + results = [] + for candidate in candidates: + results.extend(ObjectDB.objects.object_search(candidate.key, + attribute_name=attribute_name, + typeclass=typeclass, + candidates=candidates, + exact=exact, + use_dbref=use_dbref)) + else: + results = ObjectDB.objects.object_search(searchdata, + attribute_name=attribute_name, + typeclass=typeclass, + candidates=candidates, + exact=exact, + use_dbref=use_dbref) + if quiet: + return results + return _AT_SEARCH_RESULT(results, self, query=searchdata, + nofound_string=nofound_string, multimatch_string=multimatch_string) def get_display_name(self, looker, **kwargs): """