diff --git a/game/gamesrc/commands/default/objmanip.py b/game/gamesrc/commands/default/objmanip.py index d13eee8349..252e7b06c3 100644 --- a/game/gamesrc/commands/default/objmanip.py +++ b/game/gamesrc/commands/default/objmanip.py @@ -711,11 +711,24 @@ class CmdOpen(ObjManipCommand): """ caller = self.caller string = "" - # check if this exit object already exists here - exit_obj = [obj for obj in location.contents - if (obj.key.lower() == exit_name.lower() and obj.db._destination)] - if exit_obj: - exit_obj = exit_obj[0] + # check if this exit object already exists. We need to + # know what the result is before we can decide what to do; + # so we deactivate the automatic error handling. This + # always returns a list. + exit_obj = caller.search(exit_name, ignore_errors=True) + if len(exit_obj) > 1: + # give error message and return + caller.search(exit_name) + return + exit_obj = exit_obj[0] + if exit_obj: + if not exit_obj.db._destination: + # we are trying to link a non-exit + string = "'%s' already exists and is not an exit!\nIf you want to convert it " + string += "to an exit, you must assign it an attribute '_destination' first." + caller.msg(string % exit_name) + return None + # we are re-linking an old exit. old_destination = exit_obj.db._destination if old_destination: string = "Exit %s already exists." % exit_name diff --git a/game/gamesrc/commands/default/syscommands.py b/game/gamesrc/commands/default/syscommands.py index 6afcf77007..d9f9af76d9 100644 --- a/game/gamesrc/commands/default/syscommands.py +++ b/game/gamesrc/commands/default/syscommands.py @@ -67,16 +67,62 @@ class SystemNoMatch(MuxCommand): # class SystemMultimatch(MuxCommand): """ - Multiple command matches + Multiple command matches. + + The cmdhandler adds a special attribute 'matches' to this + system command. + + matches = [(candidate, cmd) , (candidate, cmd), ...], + + where candidate is an instance of src.commands.cmdparser.CommandCandidate + and cmd is an an instantiated Command object matching the candidate. """ key = CMD_MULTIMATCH + + def format_multimatches(self, caller, matches): + """ + Format multiple command matches to a useful error. + + This is copied directly from the default method in + src.commands.cmdhandler. + + """ + string = "There where multiple matches:" + for num, match in enumerate(matches): + # each match is a tuple (candidate, cmd) + candidate, cmd = match + + is_channel = hasattr(cmd, "is_channel") and cmd.is_channel + if is_channel: + is_channel = " (channel)" + else: + is_channel = "" + is_exit = hasattr(cmd, "is_exit") and cmd.is_exit + if is_exit and cmd.destination: + is_exit = " (exit to %s)" % cmd.destination + else: + is_exit = "" + + id1 = "" + id2 = "" + if not (is_channel or is_exit) and (hasattr(cmd, 'obj') and cmd.obj != caller): + # the command is defined on some other object + id1 = "%s-" % cmd.obj.name + id2 = " (%s-%s)" % (num + 1, candidate.cmdname) + else: + id1 = "%s-" % (num + 1) + id2 = "" + string += "\n %s%s%s%s%s" % (id1, candidate.cmdname, id2, is_channel, is_exit) + return string def func(self): """ argument to cmd is a comma-separated string of all the clashing matches. """ - self.caller.msg("Multiple matches found:\n %s" % self.args) + string = self.format_multimatches(self.caller, self.matches) + self.caller.msg(string) + class SystemNoPerm(MuxCommand): """ diff --git a/src/commands/cmdhandler.py b/src/commands/cmdhandler.py index c8ae2665ef..4781afa5f3 100644 --- a/src/commands/cmdhandler.py +++ b/src/commands/cmdhandler.py @@ -101,7 +101,6 @@ def get_and_merge_cmdsets(caller): exit_cmdset = None local_objects_cmdsets = [None] - #print "cmdset flags:", caller_cmdset.no_channels, caller_cmdset.no_exits, caller_cmdset.no_objs if not caller_cmdset.no_channels: # Make cmdsets out of all valid channels channel_cmdset = CHANNELHANDLER.get_cmdset(caller) @@ -110,20 +109,25 @@ def get_and_merge_cmdsets(caller): exit_cmdset = EXITHANDLER.get_cmdset(caller) location = caller.location if location and not caller_cmdset.no_objs: - # Gather all cmdsets stored on objects in the room + # Gather all cmdsets stored on objects in the room. local_objlist = location.contents local_objects_cmdsets = [obj.cmdset.current for obj in local_objlist - if obj.cmdset.outside_access - and obj.cmdset.allow_outside_access(caller)] + if obj.cmdset.allow_outside_access(caller)] # Merge all command sets into one # (the order matters, the higher-prio cmdsets are merged last) cmdset = caller_cmdset for obj_cmdset in local_objects_cmdsets: + # Here only, object cmdsets are merged with duplicates=True + # (or we would never be able to differentiate between objects) try: + old_duplicate_flag = obj_cmdset.duplicates + obj_cmdset.duplicates = True cmdset = obj_cmdset + cmdset + obj_cmdset.duplicates = old_duplicate_flag except TypeError: pass + # Exits and channels automatically has duplicates=True. try: cmdset = exit_cmdset + cmdset except TypeError: @@ -146,54 +150,113 @@ def match_command(cmd_candidates, cmdset, logged_caller=None): # Searching possible command matches in the given cmdset matches = [] prev_found_cmds = [] # to avoid aliases clashing with themselves - for cmd_candidate in cmd_candidates: + for cmd_candidate in cmd_candidates: cmdmatches = list(set([cmd for cmd in cmdset if cmd == cmd_candidate.cmdname and cmd not in prev_found_cmds])) matches.extend([(cmd_candidate, cmd) for cmd in cmdmatches]) prev_found_cmds.extend(cmdmatches) - if not matches or len(matches) == 1: + if not matches or len(matches) == 1: return matches - # Do our damndest to resolve multiple matches + # Do our damndest to resolve multiple matches ... - # First try candidate priority to separate them + # At this point we might still have several cmd candidates, + # each with a cmd match. We try to use candidate priority to + # separate them (for example this will give precedences to + # multi-word matches rather than one-word ones). + top_ranked = [] - top_priority = None + top_priority = None for match in matches: - if top_priority == None \ - or match[0].priority >= top_priority: - top_priority = match[0].priority - top_ranked.append(match) + prio = match[0].priority + if top_priority == None or prio > top_priority: + top_ranked = [match] + top_priority = prio + elif top_priority == prio: + top_ranked.append(match) + matches = top_ranked - if not matches or len(matches) == 1: + if not matches or len(matches) == 1: return matches - # still multiplies. Check if player supplied - # an obj name on the command line. We know they - # all have at least the same cmdname and obj_key - # at this point. + # Still multiplies. At this point we should have sorted out + # all candidate multiples; the multiple comes from one candidate + # matching more than one command. + + # Check if player supplied + # an obj name on the command line (e.g. 'clock's open' would + # with the default parser tell us we want the open command + # associated with the clock and not, say, the open command on + # the door in the same location). It's up to the cmdparser to + # interpret and store this reference in candidate.obj_key if given. if logged_caller: try: local_objlist = logged_caller.location.contents - match = matches[0] - top_ranked = [obj for obj in local_objlist - if match[0].obj_key == obj.name - and any(cmd == match[0].cmdname - for cmd in obj.cmdset.current)] + top_ranked = [] + candidate = matches[0][0] # all candidates should be the same + top_ranked.extend([(candidate, obj.cmdset.current.get(candidate.cmdname)) + for obj in local_objlist + if candidate.obj_key == obj.name + and any(cmd == candidate.cmdname + for cmd in obj.cmdset.current)]) if top_ranked: - matches = \ - [(match[0], - obj.cmdset.current.get(match[0].cmdname)) - for obj in top_ranked] + matches = top_ranked except Exception: logger.log_trace() + if not matches or len(matches) == 1: + return matches - # regardless what we have at this point, we have to be content + # We should still have only one candidate type, but matching + # several same-named commands. + + # Maybe the player tried to supply a separator in the form + # of a number (e.g. 1-door, 2-door for two different door exits)? If so, + # we pick the Nth-1 multiple as our result. It is up to the cmdparser + # to read and store this number in candidate.obj_key if given. + + candidate = matches[0][0] # all candidates should be the same + if candidate.obj_key and candidate.obj_key.isdigit(): + num = int(candidate.obj_key) - 1 + if 0 <= num < len(matches): + matches = [matches[num]] + + # regardless what we have at this point, we have to be content return matches +def format_multimatches(caller, matches): + """ + Format multiple command matches to a useful error. + """ + string = "There where multiple matches:" + for num, match in enumerate(matches): + # each match is a tuple (candidate, cmd) + candidate, cmd = match + + is_channel = hasattr(cmd, "is_channel") and cmd.is_channel + if is_channel: + is_channel = " (channel)" + else: + is_channel = "" + is_exit = hasattr(cmd, "is_exit") and cmd.is_exit + if is_exit and cmd.destination: + is_exit = " (exit to %s)" % cmd.destination + else: + is_exit = "" + + id1 = "" + id2 = "" + if not (is_channel or is_exit) and (hasattr(cmd, 'obj') and cmd.obj != caller): + # the command is defined on some other object + id1 = "%s-" % cmd.obj.name + id2 = " (%s-%s)" % (num + 1, candidate.cmdname) + else: + id1 = "%s-" % (num + 1) + id2 = "" + string += "\n %s%s%s%s%s" % (id1, candidate.cmdname, id2, is_channel, is_exit) + return string # Main command-handler function @@ -252,14 +315,12 @@ def cmdhandler(caller, raw_string, unloggedin=False): if len(matches) > 1: # We have a multiple-match - syscmd = cmdset.get(CMD_MULTIMATCH) - matchstring = ", ".join([match[0].cmdname - for match in matches]) + syscmd = cmdset.get(CMD_MULTIMATCH) + sysarg = "There where multiple matches." if syscmd: - sysarg = matchstring + syscmd.matches = matches else: - sysarg = "There were multiple matches:\n %s" - sysarg = sysarg % matchstring + sysarg = format_multimatches(caller, matches) raise ExecSystemCommand(syscmd, sysarg) # At this point, we have a unique command match. diff --git a/src/commands/cmdparser.py b/src/commands/cmdparser.py index f128539312..b0e967ea07 100644 --- a/src/commands/cmdparser.py +++ b/src/commands/cmdparser.py @@ -19,6 +19,7 @@ SPECIAL_CHARS = ["/", "\\", "'", '"', ":", ";", "\-", '#', '=', '!'] # Pre-compiling the regular expression is more effective REGEX = re.compile(r"""["%s"]""" % ("".join(SPECIAL_CHARS))) + class CommandCandidate(object): """ This is a convenient container for one possible @@ -32,7 +33,9 @@ class CommandCandidate(object): self.priority = priority self.obj_key = obj_key def __str__(self): - return "" % (self.cmdname, self.args) + string = "cmdcandidate 's []cmdname[ cmdname2 cmdname3 ...][] [the rest] + There are two optional forms: + -[]cmdname[ cmdname2 cmdname3 ...][] [the rest] + -[]cmdname[ cmdname2 cmdname3 ...][] [the rest] + + This allows for the user to manually choose between unresolvable + command matches. The main use for this is probably for Exit-commands. + The - identifier is used to differentiate between same-named + commands on different objects. E.g. if a 'watch' and a 'door' both + have a command 'open' defined on them, the user could differentiate + between them with + > watch-open + Alternatively, if they know (and the Multiple-match error reports + it correctly), the number among the multiples may be picked with + the - identifier: + > 2-open - This is to be used for object command sets with the 'duplicate' flag - set. It allows the player to define a particular object by name. - This object name(without the 's) will be stored as obj_key in the - CommandCandidates object and one version of the command name will be added - that lack this first part. If a command exists that has the same - name (including the 's), that command will be used - instead. Observe that the player setting will not override - normal commandset priorities - it's only used if there is no other - way to differentiate between commands (e.g. two objects in the - room both having the exact same command names and priorities). """ def produce_candidates(nr_candidates, wordlist): "Helper function" candidates = [] cmdwords_list = [] - #print "wordlist:",wordlist for n_words in range(nr_candidates): cmdwords_list.append(wordlist.pop(0)) cmdwords = " ".join([word.strip().lower() @@ -115,21 +120,22 @@ def cmdparser(raw_string): candidates.append(CommandCandidate(cmdwords, args, priority=n_words)) return candidates - raw_string = raw_string.strip() - #TODO: check for non-standard characters. - + raw_string = raw_string.strip() candidates = [] regex_result = REGEX.search(raw_string) + if not regex_result == None: # there are characters from SPECIAL_CHARS in the string. # since they cannot be part of a longer command, these # will cut short the command, no matter how long we # allow commands to be. + end_index = regex_result.start() end_char = raw_string[end_index] + if end_index == 0: - # There is one exception: if the input begins with + # There is one exception: if the input *begins* with # a special char, we let that be the command name. cmdwords = end_char if len(raw_string) > 1: @@ -140,22 +146,18 @@ def cmdparser(raw_string): return candidates else: # the special char occurred somewhere inside the string - if end_char == "'" and \ - len(raw_string) > end_index+1 and \ - raw_string[end_index+1:end_index+3] == "s ": - # The command is of the form "'s ". The - # player might have made an attempt at identifying the - # object of which's cmdtable we should prefer (e.g. - # > red door's button). + if end_char == "-" and len(raw_string) > end_index+1: + # the command is on the forms "-command" + # or "-command" obj_key = raw_string[:end_index] - alt_string = raw_string[end_index+2:] - alt_candidates = cmdparser(alt_string) - for candidate in alt_candidates: + alt_string = raw_string[end_index+1:] + for candidate in cmdparser(alt_string): candidate.obj_key = obj_key - candidates.extend(alt_candidates) - # now we let the parser continue as normal, in case - # the 's -business was not meant to be an obj ref at all. - + candidate.priority =- 1 + candidates.append(candidate) + + # We have dealt with the special possibilities. We now continue + # in case they where just accidental. # We only run the command finder up until the end char nr_candidates = len(raw_string[:end_index].split(None)) if nr_candidates <= COMMAND_MAXLEN: diff --git a/src/commands/cmdsethandler.py b/src/commands/cmdsethandler.py index b0ae7a59ba..f9cc85fbc8 100644 --- a/src/commands/cmdsethandler.py +++ b/src/commands/cmdsethandler.py @@ -210,7 +210,7 @@ class CmdSetHandler(object): for player-object handlers, which are only available to the player herself. Handle individual permission checks with the command.permissions mechanic instead. - """ + """ return self.outside_access or self.obj == source_object def update(self): diff --git a/src/comms/channelhandler.py b/src/comms/channelhandler.py index e1bc9071f8..577ec27941 100644 --- a/src/comms/channelhandler.py +++ b/src/comms/channelhandler.py @@ -130,6 +130,7 @@ class ChannelHandler(object): chan_cmdset = cmdset.CmdSet() chan_cmdset.key = '_channelset' chan_cmdset.priority = 10 + chan_cmdset.duplicates = True for cmd in [cmd for cmd in self.cached_channel_cmds if has_perm(source_object, cmd, 'chan_send')]: chan_cmdset.add(cmd) diff --git a/src/objects/exithandler.py b/src/objects/exithandler.py index 5f859f141f..6ce105572a 100644 --- a/src/objects/exithandler.py +++ b/src/objects/exithandler.py @@ -58,6 +58,7 @@ class ExitHandler(object): exit_cmdset = cmdset.CmdSet(None) exit_cmdset.key = '_exitset' exit_cmdset.priority = 9 + exit_cmdset.duplicates = True try: location = srcobj.location except Exception: diff --git a/src/objects/objects.py b/src/objects/objects.py index 2f0c85e4e5..b63754ad51 100644 --- a/src/objects/objects.py +++ b/src/objects/objects.py @@ -52,6 +52,8 @@ class Object(TypeClass): create_scripts = True if create_cmdset: dbobj.cmdset = CmdSetHandler(dbobj) + if dbobj.player: + dbobj.cmdset.outside_access = False if create_scripts: dbobj.scripts = ScriptHandler(dbobj) @@ -321,13 +323,12 @@ class Character(Object): """ from settings import CMDSET_DEFAULT self.cmdset.add_default(CMDSET_DEFAULT, permanent=True) - # this makes sure other objects are not accessing our - # command sets as they would any other object's sets. - self.cmdset.outside_access = False def at_after_move(self, source_location): "Default is to look around after a move." self.execute_cmd('look') + + # # Base Room object diff --git a/src/server/initial_setup.py b/src/server/initial_setup.py index a2a9c0f815..ed6bdc6e17 100644 --- a/src/server/initial_setup.py +++ b/src/server/initial_setup.py @@ -81,7 +81,7 @@ def create_objects(): typeclass=character_typeclass, user=god_user) god_character.id = 1 - god_character.attr('desc', 'You are Player #1.') + god_character.db.desc = 'This is User #1.' god_character.save() # Limbo is the initial starting room. @@ -93,7 +93,7 @@ def create_objects(): string += " From here you are ready to begin development." string += " If you should need help or would like to participate" string += " in community discussions, visit http://evennia.com." - limbo_obj.attr('desc', string) + limbo_obj.db.desc = string limbo_obj.save() # Now that Limbo exists, set the user up in Limbo.