diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ba5309eda..9b889f0598 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,8 @@ without arguments starts a full interactive Python console. - Make `Object/Room/Exit.create`'s `account` argument optional. If not given, will set perms to that of the object itself (along with normal Admin/Dev permission). - Make `INLINEFUNC_STACK_MAXSIZE` default visible in `settings_default.py`. +- Change how `ic` finds puppets; non-priveleged users will use `_playable_characters` list as + candidates, Builders+ will use list, local search and only global search if no match found. ## Evennia 0.9 (2018-2019) diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index 654da9924f..a894638dc2 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -941,6 +941,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): nofound_string=None, multimatch_string=None, use_nicks=True, + quiet=False, **kwargs, ): """ @@ -967,9 +968,13 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): message to echo if `searchdata` leads to multiple matches. If not given, will fall back to the default handler. use_nicks (bool, optional): Use account-level nick replacement. + quiet (bool, optional): If set, will not show any error to the user, + and will also lead to returning a list of matches. Return: match (Account, Object or None): A single Account or Object match. + list: If `quiet=True` this is a list of 0, 1 or more Account or Object matches. + Notes: Extra keywords are ignored, but are allowed in call in order to make API more consistent with @@ -981,28 +986,33 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): # handle wrapping of common terms if searchdata.lower() in ("me", "*me", "self", "*self"): return self + searchdata = self.nicks.nickreplace( + searchdata, categories=("account",), include_account=False + ) if search_object: matches = ObjectDB.objects.object_search( - searchdata, typeclass=typeclass, use_nicks=use_nicks + searchdata, typeclass=typeclass ) else: - searchdata = self.nicks.nickreplace( - searchdata, categories=("account",), include_account=False - ) - matches = AccountDB.objects.account_search(searchdata, typeclass=typeclass) - matches = _AT_SEARCH_RESULT( - matches, - self, - query=searchdata, - nofound_string=nofound_string, - multimatch_string=multimatch_string, - ) - if matches and return_puppet: - try: - return matches.puppet - except AttributeError: - return None + + if quiet: + matches = list(matches) + if return_puppet: + matches = [match.puppet for match in matches] + else: + matches = _AT_SEARCH_RESULT( + matches, + self, + query=searchdata, + nofound_string=nofound_string, + multimatch_string=multimatch_string, + ) + if matches and return_puppet: + try: + matches = matches.puppet + except AttributeError: + return None return matches def access( diff --git a/evennia/commands/default/account.py b/evennia/commands/default/account.py index 6f199a8a30..c992d18a66 100644 --- a/evennia/commands/default/account.py +++ b/evennia/commands/default/account.py @@ -301,27 +301,60 @@ class CmdIC(COMMAND_DEFAULT_CLASS): session = self.session new_character = None + character_candidates = [] + if not self.args: - new_character = account.db._last_puppet - if not new_character: + character_candidates = [account.db._last_puppet] or [] + if not character_candidates: self.msg("Usage: ic ") return - if not new_character: - # search for a matching character - new_character = [ - char for char in search.object_search(self.args) if char.access(account, "puppet") - ] - if not new_character: - self.msg("That is not a valid character choice.") - return - if len(new_character) > 1: - self.msg( - "Multiple targets with the same name:\n %s" - % ", ".join("%s(#%s)" % (obj.key, obj.id) for obj in new_character) + else: + # argument given + + if account.db._playable_characters: + # look at the playable_characters list first + character_candidates.extend( + account.search(self.args, candidates=account.db._playable_characters, + search_object=True, quiet=True) ) - return - else: - new_character = new_character[0] + + if account.locks.check_lockstring(account, "perm(Builder)"): + # builders and higher should be able to puppet more than their + # playable characters. + if session.puppet: + # start by local search - this helps to avoid the user + # getting locked into their playable characters should one + # happen to be named the same as another. We replace the suggestion + # from playable_characters here - this allows builders to puppet objects + # with the same name as their playable chars should it be necessary + # (by going to the same location). + character_candidates = [ + char + for char in session.puppet.search(self.args, quiet=True) + if char.access(account, "puppet") + ] + if not character_candidates: + # fall back to global search only if Builder+ has no + # playable_characers in list and is not standing in a room + # with a matching char. + character_candidates.extend([ + char for char in search.object_search(self.args) if char.access(account, "puppet")] + ) + + # handle possible candidates + if not character_candidates: + self.msg("That is not a valid character choice.") + return + if len(character_candidates) > 1: + self.msg( + "Multiple targets with the same name:\n %s" + % ", ".join("%s(#%s)" % (obj.key, obj.id) for obj in character_candidates) + ) + return + else: + new_character = character_candidates[0] + + # do the puppet puppet try: account.puppet_object(session, new_character) account.db._last_puppet = new_character diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index e4978dd879..5fd1bb9149 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -353,11 +353,27 @@ class TestAccount(CommandTest): self.call(account.CmdOOC(), "", "You go OOC.", caller=self.account) def test_ic(self): + self.account.db._playable_characters = [self.char1] self.account.unpuppet_object(self.session) self.call( account.CmdIC(), "Char", "You become Char.", caller=self.account, receiver=self.char1 ) + def test_ic__other_object(self): + self.account.db._playable_characters = [self.obj1] + self.account.unpuppet_object(self.session) + self.call( + account.CmdIC(), "Obj", "You become Obj.", caller=self.account, receiver=self.obj1 + ) + + def test_ic__nonaccess(self): + self.account.unpuppet_object(self.session) + self.call( + account.CmdIC(), "Nonexistent", "That is not a valid character choice.", + caller=self.account, receiver=self.account + ) + + def test_password(self): self.call( account.CmdPassword(), diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index d2bb4c2b2c..018ed61c8b 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -389,8 +389,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): - `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. + global_search (bool): Search all objects globally. This overrules 'location' data. 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